From 6bb7ab261a01f371173cd8c4820e712eee3de267 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Tue, 19 Mar 2024 16:32:24 +0000 Subject: [PATCH] AzureMonitor: User authentication support (#81918) * Stub out frontend user auth * Stub out backend user auth * Add context * Reorganise files * Refactor app registration form * Alert for user auth service principal credentials * AzureMonitor: Add flag for enabling/disabling fallback credentials for current user authentication (#82332) * Rename field * Add fallback setting * Update tests and mock * Remove duplicate setting line * Update name of property * Update frontend settings * Update docs and default config files * Update azure-sdk * Fix lint * Update test * Bump dependency * Update configuration * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Docs review * AzureMonitor: User authentication frontend updates (#83107) * Rename field * Add fallback setting * Update tests and mock * Remove duplicate setting line * Update name of property * Update frontend settings * Update docs and default config files * Add alerts to query editor - Add authenticatedBy property to grafana/data - Update mocks - Update query editor to disable it under certain circumstances - Update tests * Add separate FallbackCredentials component - Reset AppRegistrationCredentials component to only handle clientsecret credentials - Update AzureCredentialsForm - Update selectors - Update tests - Update credentials utility functions logic * Alert when fallback credentials disabled * Update condition * Update azure-sdk * Fix lint * Update test * Remove unneeded conditions * Set auth type correctly * Legacy cloud options * Fix client secret * Remove accidental import * Bump dependency * Add tests * Don't use VerticalGroup component * Remove unused import * Fix lint * Appropriately set oAuthPassThru and disableGrafanaCache properties * Clear azureCredentials on authType change * Correctly retrieve secret * Fix bug in authTypeOptions * Update public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> * Update public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> * Update public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> * Add documentation links * Fix broken link --------- Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> * AzureMonitor: Update docs for current user authentication (#83440) * Rename field * Add fallback setting * Update tests and mock * Remove duplicate setting line * Update name of property * Update frontend settings * Update docs and default config files * Add alerts to query editor - Add authenticatedBy property to grafana/data - Update mocks - Update query editor to disable it under certain circumstances - Update tests * Add separate FallbackCredentials component - Reset AppRegistrationCredentials component to only handle clientsecret credentials - Update AzureCredentialsForm - Update selectors - Update tests - Update credentials utility functions logic * Alert when fallback credentials disabled * Update condition * Update azure-sdk * Fix lint * Update test * Remove unneeded conditions * Set auth type correctly * Legacy cloud options * Fix client secret * Remove accidental import * Bump dependency * Add tests * Don't use VerticalGroup component * Remove unused import * Update docs * Fix lint * Appropriately set oAuthPassThru and disableGrafanaCache properties * Clear azureCredentials on authType change * Correctly retrieve secret * Feedback * Spelling * Update docs/sources/datasources/azure-monitor/_index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Update docs/sources/datasources/azure-monitor/_index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Update docs/sources/datasources/azure-monitor/_index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Update docs/sources/datasources/azure-monitor/_index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> --------- Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Docs review * Update docs with additional configuration information * Fix to appropriately hide the query editor * Typo * Update isCredentialsComplete * Update test --------- Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> --- conf/defaults.ini | 7 + conf/sample.ini | 7 + .../datasources/azure-monitor/_index.md | 96 ++++++++ .../setup-grafana/configure-grafana/_index.md | 6 + packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 2 + pkg/api/dtos/frontend_settings.go | 9 +- pkg/api/frontendsettings.go | 9 +- .../pluginconfig/request.go | 1 + .../pluginconfig/request_test.go | 46 ++-- pkg/setting/setting_azure.go | 1 + pkg/setting/setting_azure_test.go | 26 ++ .../azuremonitor/azmoncredentials/builder.go | 21 ++ pkg/tsdb/azuremonitor/azuremonitor.go | 38 +-- pkg/tsdb/azuremonitor/httpclient.go | 1 + .../azuremonitor/__mocks__/datasource.ts | 1 + .../ArgQueryEditor/SubscriptionField.tsx | 2 +- .../components/AzureCredentialsForm.tsx | 231 ------------------ .../AppRegistrationCredentials.tsx | 149 +++++++++++ .../AzureCredentialsForm.test.tsx | 3 +- .../ConfigEditor/AzureCredentialsForm.tsx | 126 ++++++++++ .../{ => ConfigEditor}/ConfigEditor.test.tsx | 0 .../{ => ConfigEditor}/ConfigEditor.tsx | 6 +- .../CurrentUserFallbackCredentials.test.tsx | 74 ++++++ .../CurrentUserFallbackCredentials.tsx | 178 ++++++++++++++ .../DefaultSubscription.test.tsx | 4 +- .../DefaultSubscription.tsx | 6 +- .../{ => ConfigEditor}/MonitorConfig.tsx | 15 +- .../{ => LogsQueryEditor}/AzureCheatSheet.tsx | 4 +- .../LogsQueryEditor/AzureCheatSheetModal.tsx | 3 +- .../LogsQueryEditor/LogsQueryEditor.tsx | 2 +- .../{ => LogsQueryEditor}/RawQuery.tsx | 0 .../{ => LogsQueryEditor}/syntax.ts | 0 .../MetricsQueryEditor/AggregationField.tsx | 2 +- .../MetricsQueryEditor/DimensionFields.tsx | 2 +- .../MetricsQueryEditor/LegendFormatField.tsx | 2 +- .../MetricsQueryEditor/MetricNameField.tsx | 2 +- .../MetricNamespaceField.tsx | 2 +- .../MetricsQueryEditor/TimeGrainField.tsx | 2 +- .../MetricsQueryEditor/TopField.tsx | 2 +- .../QueryEditor/QueryEditor.test.tsx | 79 ++++++ .../components/QueryEditor/QueryEditor.tsx | 65 ++++- .../{ => QueryEditor}/QueryHeader.tsx | 4 +- .../ResourceField/ResourceField.tsx | 2 +- .../TracesQueryEditor/TraceTypeField.tsx | 2 +- .../TracesQueryEditor/TracesQueryEditor.tsx | 4 +- .../components/{ => shared}/Field.tsx | 0 .../{ => shared}/FormatAsField.test.tsx | 8 +- .../components/{ => shared}/FormatAsField.tsx | 4 +- .../datasource/azuremonitor/credentials.ts | 84 ++++++- .../datasource/azuremonitor/datasource.ts | 17 +- .../datasource/azuremonitor/e2e/selectors.ts | 5 + .../plugins/datasource/azuremonitor/module.ts | 2 +- .../datasource/azuremonitor/types/types.ts | 13 +- .../mssql/azureauth/AzureAuth.testMocks.ts | 8 +- 55 files changed, 1058 insertions(+), 328 deletions(-) delete mode 100644 public/app/plugins/datasource/azuremonitor/components/AzureCredentialsForm.tsx create mode 100644 public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx rename public/app/plugins/datasource/azuremonitor/components/{ => ConfigEditor}/AzureCredentialsForm.test.tsx (96%) create mode 100644 public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx rename public/app/plugins/datasource/azuremonitor/components/{ => ConfigEditor}/ConfigEditor.test.tsx (100%) rename public/app/plugins/datasource/azuremonitor/components/{ => ConfigEditor}/ConfigEditor.tsx (96%) create mode 100644 public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.test.tsx create mode 100644 public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx rename public/app/plugins/datasource/azuremonitor/components/{ => ConfigEditor}/DefaultSubscription.test.tsx (94%) rename public/app/plugins/datasource/azuremonitor/components/{ => ConfigEditor}/DefaultSubscription.tsx (94%) rename public/app/plugins/datasource/azuremonitor/components/{ => ConfigEditor}/MonitorConfig.tsx (82%) rename public/app/plugins/datasource/azuremonitor/components/{ => LogsQueryEditor}/AzureCheatSheet.tsx (98%) rename public/app/plugins/datasource/azuremonitor/components/{ => LogsQueryEditor}/RawQuery.tsx (100%) rename public/app/plugins/datasource/azuremonitor/components/{ => LogsQueryEditor}/syntax.ts (100%) rename public/app/plugins/datasource/azuremonitor/components/{ => QueryEditor}/QueryHeader.tsx (92%) rename public/app/plugins/datasource/azuremonitor/components/{ => shared}/Field.tsx (100%) rename public/app/plugins/datasource/azuremonitor/components/{ => shared}/FormatAsField.test.tsx (89%) rename public/app/plugins/datasource/azuremonitor/components/{ => shared}/FormatAsField.tsx (92%) diff --git a/conf/defaults.ini b/conf/defaults.ini index 001cbbce61c..304928eed26 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -940,6 +940,13 @@ workload_identity_token_file = # Disabled by default, needs to be explicitly enabled user_identity_enabled = false +# Specifies whether user identity authentication fallback credentials should be enabled in data sources +# Enabling this allows data source creators to provide fallback credentials for backend initiated requests +# e.g. alerting, recorded queries etc. +# Enabled by default, needs to be explicitly disabled +# Will not have any effect if user identity is disabled above +user_identity_fallback_credentials_enabled = true + # Override token URL for Azure Active Directory # By default is the same as token URL configured for AAD authentication settings user_identity_token_url = diff --git a/conf/sample.ini b/conf/sample.ini index 1c52da72f8d..36ca32eea82 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -863,6 +863,13 @@ # Disabled by default, needs to be explicitly enabled ;user_identity_enabled = false +# Specifies whether user identity authentication fallback credentials should be enabled in data sources +# Enabling this allows data source creators to provide fallback credentials for backend initiated requests +# e.g. alerting, recorded queries etc. +# Enabled by default, needs to be explicitly disabled +# Will not have any effect if user identity is disabled above +;user_identity_fallback_credentials_enabled = true + # Override token URL for Azure Active Directory # By default is the same as token URL configured for AAD authentication settings ;user_identity_token_url = diff --git a/docs/sources/datasources/azure-monitor/_index.md b/docs/sources/datasources/azure-monitor/_index.md index 4579af5e1da..1ca7aebe514 100644 --- a/docs/sources/datasources/azure-monitor/_index.md +++ b/docs/sources/datasources/azure-monitor/_index.md @@ -133,6 +133,28 @@ datasources: version: 1 ``` +**Current User:** + +{{< admonition type="note" >}} +The `oauthPassThru` property is required for current user authentication to function. +Additionally, `disableGrafanaCache` is necessary to prevent the data source returning cached responses for resources users don't have access to. +{{< /admonition >}} + +```yaml +apiVersion: 1 # config file version + +datasources: + - name: Azure Monitor + type: grafana-azure-monitor-datasource + access: proxy + jsonData: + azureAuthType: currentuser + oauthPassThru: true + disableGrafanaCache: true + subscriptionId: # Optional, default subscription + version: 1 +``` + #### Supported cloud names | Azure Cloud | `cloudName` Value | @@ -141,6 +163,11 @@ datasources: | **Microsoft Chinese national cloud** | `chinaazuremonitor` | | **US Government cloud** | `govazuremonitor` | +{{< admonition type="note" >}} +Cloud names for current user authentication differ to the `cloudName` values in the preceding table. +The public cloud name is `AzureCloud`, the Chinese national cloud name is `AzureChinaCloud`, and the US Government cloud name is `AzureUSGovernment`. +{{< /admonition >}} + ### Configure Managed Identity You can use managed identity to configure Azure Monitor in Grafana if you host Grafana in Azure (such as an App Service or with Azure Virtual Machines) and have managed identity enabled on your VM. @@ -201,6 +228,66 @@ For details on workload identity, refer to the [Azure workload identity document workload_identity_token_file = TOKEN_FILE_PATH ``` +### Configure Current User authentication + +{{< admonition type="note" >}} +Current user authentication is an [experimental feature](/docs/release-life-cycle). Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Contact Grafana Support to enable this feature in Grafana Cloud. Aspects of Grafana may not work as expected when using this authentication method. +{{< /admonition >}} + +If your Grafana instance is configured with Azure Entra (formerly Active Directory) authentication for login, this authentication method can be used to forward the currently logged in user's credentials to the data source. The users credentials will then be used when requesting data from the data source. For details on how to configure your Grafana instance using Azure Entra refer to the [documentation][configure-grafana-azure-auth]. + +{{< admonition type="note" >}} +Additional configuration is required to ensure that the App Registration used to login a user via Azure provides an access token with the permissions required by the data source. + +The App Registration must be configured to issue both **Access Tokens** and **ID Tokens**. + +1. In the Azure Portal, open the App Registration that requires configuration. +2. Select **Authentication** in the side menu. +3. Under **Implicit grant and hybrid flows** check both the **Access tokens** and **ID tokens** boxes. +4. Save the changes to ensure the App Registration is updated. + +The App Registration must also be configured with additional **API Permissions** to provide authenticated users with access to the APIs utilised by the data source. + +1. In the Azure Portal, open the App Registration that requires configuration. +1. Select **API Permissions** in the side menu. +1. Ensure the `openid`, `profile`, `email`, and `offline_access` permissions are present under the **Microsoft Graph** section. If not, they must be added. +1. Select **Add a permission** and choose the following permissions. They must be added individually. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) for more information. + - Select **Azure Service Management** > **Delegated permissions** > `user_impersonation` > **Add permissions** + - Select **APIs my organization uses** > Search for **Log Analytics API** and select it > **Delegated permissions** > `Date.Read` > **Add permissions** + +Once all permissions have been added, the Azure authentication section in Grafana must be updated. The `scopes` section must be updated to include the `.default` scope to ensure that a token with access to all APIs declared on the App Registration is requested by Grafana. Once updated the scopes value should equal: `.default openid email profile`. +{{< /admonition >}} + +This method of authentication doesn't inherently support all backend functionality as a user's credentials won't be in scope. +Affected functionality includes alerting, reporting, and recorded queries. +In order to support backend queries when using a data source configured with current user authentication, you can configure service credentials. +Also, note that query and resource caching is disabled by default for data sources using current user authentication. + +{{< admonition type="note" >}} +To configure fallback service credentials the [feature toggle][configure-grafana-feature-toggles] `idForwarding` must be set to `true` and `user_identity_fallback_credentials_enabled` must be enabled in the [Azure configuration section][configure-grafana-azure] (enabled by default when `user_identity_enabled` is set to `true`). +{{< /admonition >}} + +Permissions for fallback credentials may need to be broad to appropriately support backend functionality. +For example, an alerting query created by a user is dependent on their permissions. +If a user tries to create an alert for a resource that the fallback credentials can't access, the alert will fail. + +**To enable current user authentication for Grafana:** + +1. Set the `user_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration][configure-grafana-azure]. + By default this will also enable fallback service credentials. + If you want to disable service credentials at the instance level set `user_identity_fallback_credentials_enabled` to false. + + ```ini + [azure] + user_identity_enabled = true + ``` + +1. In the Azure Monitor data source configuration, set **Authentication** to **Current User**. + If fallback service credentials are enabled at the instance level, an additional configuration section is visible that you can use to enable or disable using service credentials for this data source. + {{< figure src="/media/docs/grafana/data-sources/screenshot-current-user.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor screenshot showing Current User authentication" >}} + +1. If you want backend functionality to work with this data source, enable service credentials and configure the data source using the most applicable credentials for your circumstances. + ## Query the data source The Azure Monitor data source can query data from Azure Monitor Metrics and Logs, the Azure Resource Graph, and Application Insights Traces. Each source has its own specialized query editor. @@ -230,6 +317,15 @@ If you're upgrading from a Grafana version prior to v9.0 and relied on Applicati [configure-grafana-azure]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#azure" [configure-grafana-azure]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#azure" +[configure-grafana-azure-auth]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread" +[configure-grafana-azure-auth]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread" + +[configure-grafana-azure-auth-scopes]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana" +[configure-grafana-azure-auth-scopes]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana" + +[configure-grafana-feature-toggles]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana/#feature_toggles" +[configure-grafana-feature-toggles]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana/#feature_toggles" + [data-source-management]: "/docs/grafana/ -> /docs/grafana//administration/data-source-management" [data-source-management]: "/docs/grafana-cloud/ -> /docs/grafana//administration/data-source-management" diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index a9a5f3437d4..20e97399c06 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1202,6 +1202,12 @@ Specifies whether user identity authentication (on behalf of currently signed-in Disabled by default, needs to be explicitly enabled. +### user_identity_fallback_credentials_enabled + +Specifies whether user identity authentication fallback credentials should be enabled in data sources. Enabling this allows data source creators to provide fallback credentials for backend-initiated requests, such as alerting, recorded queries, and so on. + +It is by default and needs to be explicitly disabled. It will not have any effect if user identity authentication is disabled. + ### user_identity_token_url Override token URL for Azure Active Directory. diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index def3242e7c5..6a67ad5c1ce 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -126,6 +126,7 @@ export interface CurrentUserDTO { language: string; permissions?: Record; analytics: AnalyticsSettings; + authenticatedBy: string; /** @deprecated Use theme instead */ lightTheme: boolean; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 8a7dae06941..3af5b15d190 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -25,6 +25,7 @@ export interface AzureSettings { managedIdentityEnabled: boolean; workloadIdentityEnabled: boolean; userIdentityEnabled: boolean; + userIdentityFallbackCredentialsEnabled: boolean; } export interface AzureCloudInfo { @@ -129,6 +130,7 @@ export class GrafanaBootConfig implements GrafanaConfig { managedIdentityEnabled: false, workloadIdentityEnabled: false, userIdentityEnabled: false, + userIdentityFallbackCredentialsEnabled: false, }; caching = { enabled: false, diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index d1b30c6fdc7..8d384ab02c7 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -65,10 +65,11 @@ type FrontendSettingsLicenseInfoDTO struct { } type FrontendSettingsAzureDTO struct { - Cloud string `json:"cloud"` - ManagedIdentityEnabled bool `json:"managedIdentityEnabled"` - WorkloadIdentityEnabled bool `json:"workloadIdentityEnabled"` - UserIdentityEnabled bool `json:"userIdentityEnabled"` + Cloud string `json:"cloud"` + ManagedIdentityEnabled bool `json:"managedIdentityEnabled"` + WorkloadIdentityEnabled bool `json:"workloadIdentityEnabled"` + UserIdentityEnabled bool `json:"userIdentityEnabled"` + UserIdentityFallbackCredentialsEnabled bool `json:"userIdentityFallbackCredentialsEnabled"` } type FrontendSettingsCachingDTO struct { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index a9956fd42b8..209972efe39 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -269,10 +269,11 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro SupportBundlesEnabled: isSupportBundlesEnabled(hs), Azure: dtos.FrontendSettingsAzureDTO{ - Cloud: hs.Cfg.Azure.Cloud, - ManagedIdentityEnabled: hs.Cfg.Azure.ManagedIdentityEnabled, - WorkloadIdentityEnabled: hs.Cfg.Azure.WorkloadIdentityEnabled, - UserIdentityEnabled: hs.Cfg.Azure.UserIdentityEnabled, + Cloud: hs.Cfg.Azure.Cloud, + ManagedIdentityEnabled: hs.Cfg.Azure.ManagedIdentityEnabled, + WorkloadIdentityEnabled: hs.Cfg.Azure.WorkloadIdentityEnabled, + UserIdentityEnabled: hs.Cfg.Azure.UserIdentityEnabled, + UserIdentityFallbackCredentialsEnabled: hs.Cfg.Azure.UserIdentityFallbackCredentialsEnabled, }, Caching: dtos.FrontendSettingsCachingDTO{ diff --git a/pkg/services/pluginsintegration/pluginconfig/request.go b/pkg/services/pluginsintegration/pluginconfig/request.go index 042a8533c2f..dfb1a5b2dd8 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request.go +++ b/pkg/services/pluginsintegration/pluginconfig/request.go @@ -101,6 +101,7 @@ func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginI if azureSettings.UserIdentityEnabled { m[azsettings.UserIdentityEnabled] = "true" + m[azsettings.UserIdentityFallbackCredentialsEnabled] = strconv.FormatBool(azureSettings.UserIdentityFallbackCredentialsEnabled) if azureSettings.UserIdentityTokenEndpoint != nil { if azureSettings.UserIdentityTokenEndpoint.TokenUrl != "" { diff --git a/pkg/services/pluginsintegration/pluginconfig/request_test.go b/pkg/services/pluginsintegration/pluginconfig/request_test.go index 3a9aab34d64..52fa6454b38 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/request_test.go @@ -276,7 +276,8 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { ClientSecret: "mock_user_identity_client_secret", UsernameAssertion: true, }, - ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"}, + UserIdentityFallbackCredentialsEnabled: true, + ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"}, } t.Run("uses the azure settings for an Azure plugin", func(t *testing.T) { @@ -289,16 +290,17 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { p := NewRequestConfigProvider(pCfg) require.Subset(t, p.PluginRequestConfig(context.Background(), "grafana-azure-monitor-datasource"), map[string]string{ "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", - "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", - "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", - "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", - "GFAZPL_USER_IDENTITY_ENABLED": "true", - "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", - "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", - "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", - "GFAZPL_USER_IDENTITY_ASSERTION": "username", + "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", + "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", + "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", + "GFAZPL_USER_IDENTITY_ENABLED": "true", + "GFAZPL_USER_IDENTITY_FALLBACK_SERVICE_CREDENTIALS_ENABLED": "true", + "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", + "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", + "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", + "GFAZPL_USER_IDENTITY_ASSERTION": "username", }) }) @@ -319,6 +321,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID") require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE") require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ENABLED") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_FALLBACK_SERVICE_CREDENTIALS_ENABLED") require.NotContains(t, m, "GFAZPL_USER_IDENTITY_TOKEN_URL") require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_ID") require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_SECRET") @@ -336,16 +339,17 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { p := NewRequestConfigProvider(pCfg) require.Subset(t, p.PluginRequestConfig(context.Background(), "test-datasource"), map[string]string{ "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", - "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", - "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", - "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", - "GFAZPL_USER_IDENTITY_ENABLED": "true", - "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", - "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", - "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", - "GFAZPL_USER_IDENTITY_ASSERTION": "username", + "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", + "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", + "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", + "GFAZPL_USER_IDENTITY_ENABLED": "true", + "GFAZPL_USER_IDENTITY_FALLBACK_SERVICE_CREDENTIALS_ENABLED": "true", + "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", + "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", + "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", + "GFAZPL_USER_IDENTITY_ASSERTION": "username", }) }) } diff --git a/pkg/setting/setting_azure.go b/pkg/setting/setting_azure.go index 4362e593b53..dc50750ccdf 100644 --- a/pkg/setting/setting_azure.go +++ b/pkg/setting/setting_azure.go @@ -66,6 +66,7 @@ func (cfg *Cfg) readAzureSettings() { } azureSettings.UserIdentityTokenEndpoint = tokenEndpointSettings + azureSettings.UserIdentityFallbackCredentialsEnabled = azureSection.Key("user_identity_fallback_credentials_enabled").MustBool(true) } azureSettings.ForwardSettingsPlugins = util.SplitString(azureSection.Key("forward_settings_to_plugins").String()) diff --git a/pkg/setting/setting_azure_test.go b/pkg/setting/setting_azure_test.go index 39354e45f42..6dd67f7313f 100644 --- a/pkg/setting/setting_azure_test.go +++ b/pkg/setting/setting_azure_test.go @@ -109,6 +109,32 @@ func TestAzureSettings(t *testing.T) { assert.True(t, cfg.Azure.UserIdentityEnabled) }) + t.Run("enables service credentials by default", func(t *testing.T) { + cfg := NewCfg() + + azureSection, err := cfg.Raw.NewSection("azure") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_enabled", "true") + require.NoError(t, err) + + cfg.readAzureSettings() + + assert.True(t, cfg.Azure.UserIdentityFallbackCredentialsEnabled) + }) + t.Run("disables service credentials", func(t *testing.T) { + cfg := NewCfg() + + azureSection, err := cfg.Raw.NewSection("azure") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_enabled", "true") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_fallback_credentials_enabled", "false") + require.NoError(t, err) + + cfg.readAzureSettings() + + assert.False(t, cfg.Azure.UserIdentityFallbackCredentialsEnabled) + }) t.Run("should use token endpoint from Azure AD if enabled", func(t *testing.T) { cfg := NewCfg() diff --git a/pkg/tsdb/azuremonitor/azmoncredentials/builder.go b/pkg/tsdb/azuremonitor/azmoncredentials/builder.go index 843326ca8ac..30c98ec4915 100644 --- a/pkg/tsdb/azuremonitor/azmoncredentials/builder.go +++ b/pkg/tsdb/azuremonitor/azmoncredentials/builder.go @@ -62,6 +62,27 @@ func getFromLegacy(data map[string]interface{}, secureData map[string]string) (a credentials := &azcredentials.AzureWorkloadIdentityCredentials{} return credentials, nil + case azcredentials.AzureAuthCurrentUserIdentity: + legacyCloud, err := maputil.GetStringOptional(data, "cloudName") + if err != nil { + return nil, err + } + cloud, err := resolveLegacyCloudName(legacyCloud) + if err != nil { + return nil, err + } + clientSecret := secureData["clientSecret"] + + credentials := &azcredentials.AadCurrentUserCredentials{ + ServiceCredentials: &azcredentials.AzureClientSecretCredentials{ + AzureCloud: cloud, + TenantId: tenantId, + ClientId: clientId, + ClientSecret: clientSecret, + }, + } + + return credentials, nil case azcredentials.AzureAuthClientSecret: legacyCloud, err := maputil.GetStringOptional(data, "cloudName") if err != nil { diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 60be6821b04..085f05426d2 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" + "github.com/grafana/grafana-azure-sdk-go/v2/azusercontext" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" @@ -52,11 +53,11 @@ func ProvideService(httpClientProvider *httpclient.Provider) *Service { } func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return s.queryMux.QueryData(ctx, req) + return s.queryMux.QueryData(azusercontext.WithUserFromQueryReq(ctx, req), req) } func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return s.resourceHandler.CallResource(ctx, req, sender) + return s.resourceHandler.CallResource(azusercontext.WithUserFromResourceReq(ctx, req), req, sender) } type Service struct { @@ -191,10 +192,10 @@ func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext return instance, nil } -func queryMetricHealth(dsInfo types.DatasourceInfo) (*http.Response, error) { +func queryMetricHealth(ctx context.Context, dsInfo types.DatasourceInfo) (*http.Response, error) { subscriptionsApiVersion := "2020-01-01" url := fmt.Sprintf("%v/subscriptions?api-version=%v", dsInfo.Routes["Azure Monitor"].URL, subscriptionsApiVersion) - request, err := http.NewRequest(http.MethodGet, url, nil) + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } @@ -207,9 +208,9 @@ func queryMetricHealth(dsInfo types.DatasourceInfo) (*http.Response, error) { return res, nil } -func checkAzureLogAnalyticsHealth(dsInfo types.DatasourceInfo, subscription string) (*http.Response, error) { +func checkAzureLogAnalyticsHealth(ctx context.Context, dsInfo types.DatasourceInfo, subscription string) (*http.Response, error) { workspacesUrl := fmt.Sprintf("%v/subscriptions/%v/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview", dsInfo.Routes["Azure Monitor"].URL, subscription) - workspacesReq, err := http.NewRequest(http.MethodGet, workspacesUrl, nil) + workspacesReq, err := http.NewRequestWithContext(ctx, http.MethodGet, workspacesUrl, nil) if err != nil { return nil, err } @@ -238,7 +239,7 @@ func checkAzureLogAnalyticsHealth(dsInfo types.DatasourceInfo, subscription stri } workspaceUrl := fmt.Sprintf("%v/v1/workspaces/%v/query", dsInfo.Routes["Azure Log Analytics"].URL, defaultWorkspaceId) - workspaceReq, err := http.NewRequest(http.MethodPost, workspaceUrl, bytes.NewBuffer(body)) + workspaceReq, err := http.NewRequestWithContext(ctx, http.MethodPost, workspaceUrl, bytes.NewBuffer(body)) workspaceReq.Header.Set("Content-Type", "application/json") if err != nil { return nil, err @@ -252,7 +253,7 @@ func checkAzureLogAnalyticsHealth(dsInfo types.DatasourceInfo, subscription stri return res, nil } -func checkAzureMonitorResourceGraphHealth(dsInfo types.DatasourceInfo, subscription string) (*http.Response, error) { +func checkAzureMonitorResourceGraphHealth(ctx context.Context, dsInfo types.DatasourceInfo, subscription string) (*http.Response, error) { body, err := json.Marshal(map[string]any{ "query": "Resources | project id | limit 1", "subscriptions": []string{subscription}, @@ -261,7 +262,7 @@ func checkAzureMonitorResourceGraphHealth(dsInfo types.DatasourceInfo, subscript return nil, err } url := fmt.Sprintf("%v/providers/Microsoft.ResourceGraph/resources?api-version=%v", dsInfo.Routes["Azure Resource Graph"].URL, resourcegraph.ArgAPIVersion) - request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) + request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) request.Header.Set("Content-Type", "application/json") if err != nil { return nil, err @@ -275,9 +276,9 @@ func checkAzureMonitorResourceGraphHealth(dsInfo types.DatasourceInfo, subscript return res, nil } -func metricCheckHealth(dsInfo types.DatasourceInfo, logger log.Logger) (message string, defaultSubscription string, status backend.HealthStatus) { +func metricCheckHealth(ctx context.Context, dsInfo types.DatasourceInfo, logger log.Logger) (message string, defaultSubscription string, status backend.HealthStatus) { defaultSubscription = dsInfo.Settings.SubscriptionId - metricsRes, err := queryMetricHealth(dsInfo) + metricsRes, err := queryMetricHealth(ctx, dsInfo) if err != nil { if ok := errors.Is(err, types.ErrorAzureHealthCheck); ok { return fmt.Sprintf("Error connecting to Azure Monitor endpoint: %s", err.Error()), defaultSubscription, backend.HealthStatusError @@ -309,8 +310,8 @@ func metricCheckHealth(dsInfo types.DatasourceInfo, logger log.Logger) (message return "Successfully connected to Azure Monitor endpoint.", defaultSubscription, backend.HealthStatusOk } -func logAnalyticsCheckHealth(dsInfo types.DatasourceInfo, defaultSubscription string) (message string, status backend.HealthStatus) { - logsRes, err := checkAzureLogAnalyticsHealth(dsInfo, defaultSubscription) +func logAnalyticsCheckHealth(ctx context.Context, dsInfo types.DatasourceInfo, defaultSubscription string) (message string, status backend.HealthStatus) { + logsRes, err := checkAzureLogAnalyticsHealth(ctx, dsInfo, defaultSubscription) if err != nil { if err.Error() == "no default workspace found" { return "No Log Analytics workspaces found.", backend.HealthStatusUnknown @@ -337,8 +338,8 @@ func logAnalyticsCheckHealth(dsInfo types.DatasourceInfo, defaultSubscription st return "Successfully connected to Azure Log Analytics endpoint.", backend.HealthStatusOk } -func graphLogHealthCheck(dsInfo types.DatasourceInfo, defaultSubscription string) (message string, status backend.HealthStatus) { - resourceGraphRes, err := checkAzureMonitorResourceGraphHealth(dsInfo, defaultSubscription) +func graphLogHealthCheck(ctx context.Context, dsInfo types.DatasourceInfo, defaultSubscription string) (message string, status backend.HealthStatus) { + resourceGraphRes, err := checkAzureMonitorResourceGraphHealth(ctx, dsInfo, defaultSubscription) if err != nil { if ok := errors.Is(err, types.ErrorAzureHealthCheck); ok { return fmt.Sprintf("Error connecting to Azure Resource Graph endpoint: %s", err.Error()), backend.HealthStatusError @@ -387,6 +388,7 @@ func parseSubscriptions(res *http.Response, logger log.Logger) ([]string, error) } func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + ctx = azusercontext.WithUserFromHealthCheckReq(ctx, req) dsInfo, err := s.getDSInfo(ctx, req.PluginContext) if err != nil { return &backend.CheckHealthResult{ @@ -397,17 +399,17 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque status := backend.HealthStatusOk - metricsLog, defaultSubscription, metricsStatus := metricCheckHealth(dsInfo, s.logger) + metricsLog, defaultSubscription, metricsStatus := metricCheckHealth(ctx, dsInfo, s.logger) if metricsStatus != backend.HealthStatusOk { status = metricsStatus } - logAnalyticsLog, logAnalyticsStatus := logAnalyticsCheckHealth(dsInfo, defaultSubscription) + logAnalyticsLog, logAnalyticsStatus := logAnalyticsCheckHealth(ctx, dsInfo, defaultSubscription) if logAnalyticsStatus != backend.HealthStatusOk { status = logAnalyticsStatus } - graphLog, graphStatus := graphLogHealthCheck(dsInfo, defaultSubscription) + graphLog, graphStatus := graphLogHealthCheck(ctx, dsInfo, defaultSubscription) if graphStatus != backend.HealthStatusOk { status = graphStatus } diff --git a/pkg/tsdb/azuremonitor/httpclient.go b/pkg/tsdb/azuremonitor/httpclient.go index 8b26645e356..53fd0735d7a 100644 --- a/pkg/tsdb/azuremonitor/httpclient.go +++ b/pkg/tsdb/azuremonitor/httpclient.go @@ -38,6 +38,7 @@ func newHTTPClient(ctx context.Context, route types.AzRoute, model types.Datasou } authOpts := azhttpclient.NewAuthOptions(azureSettings) + authOpts.AllowUserIdentity() authOpts.Scopes(route.Scopes) azhttpclient.AddAzureAuthentication(&clientOpts, authOpts, model.Credentials) } diff --git a/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts b/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts index 170ee9e5618..a51004bf812 100644 --- a/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts @@ -75,6 +75,7 @@ export default function createMockDatasource(overrides?: DeepPartial getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}), }, getVariablesRaw: jest.fn().mockReturnValue([]), + currentUserAuth: false, ...overrides, }; diff --git a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx index 7b03af248bc..019edbf1915 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx @@ -6,7 +6,7 @@ import { FieldValidationMessage, MultiSelect } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; import { AzureMonitorQuery, AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; import { findOptions } from '../../utils/common'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; interface SubscriptionFieldProps extends AzureQueryEditorFieldProps { onQueryChange: (newQuery: AzureMonitorQuery) => void; diff --git a/public/app/plugins/datasource/azuremonitor/components/AzureCredentialsForm.tsx b/public/app/plugins/datasource/azuremonitor/components/AzureCredentialsForm.tsx deleted file mode 100644 index 80fe46c8302..00000000000 --- a/public/app/plugins/datasource/azuremonitor/components/AzureCredentialsForm.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import React, { ChangeEvent, useMemo } from 'react'; - -import { SelectableValue } from '@grafana/data'; -import { ConfigSection } from '@grafana/experimental'; -import { Button, Select, Field, Input } from '@grafana/ui'; - -import { selectors } from '../e2e/selectors'; -import { AzureAuthType, AzureCredentials } from '../types'; - -export interface Props { - managedIdentityEnabled: boolean; - workloadIdentityEnabled: boolean; - credentials: AzureCredentials; - azureCloudOptions?: SelectableValue[]; - onCredentialsChange: (updatedCredentials: AzureCredentials) => void; - disabled?: boolean; - children?: JSX.Element; -} - -export const AzureCredentialsForm = (props: Props) => { - const { - credentials, - azureCloudOptions, - onCredentialsChange, - disabled, - managedIdentityEnabled, - workloadIdentityEnabled, - } = props; - - const authTypeOptions = useMemo(() => { - let opts: Array> = [ - { - value: 'clientsecret', - label: 'App Registration', - }, - ]; - - if (managedIdentityEnabled) { - opts.push({ - value: 'msi', - label: 'Managed Identity', - }); - } - - if (workloadIdentityEnabled) { - opts.push({ - value: 'workloadidentity', - label: 'Workload Identity', - }); - } - - return opts; - }, [managedIdentityEnabled, workloadIdentityEnabled]); - - const onAuthTypeChange = (selected: SelectableValue) => { - const defaultAuthType = managedIdentityEnabled - ? 'msi' - : workloadIdentityEnabled - ? 'workloadidentity' - : 'clientsecret'; - const updated: AzureCredentials = { - ...credentials, - authType: selected.value || defaultAuthType, - }; - onCredentialsChange(updated); - }; - - const onAzureCloudChange = (selected: SelectableValue) => { - if (credentials.authType === 'clientsecret') { - const updated: AzureCredentials = { - ...credentials, - azureCloud: selected.value, - }; - onCredentialsChange(updated); - } - }; - - const onTenantIdChange = (event: ChangeEvent) => { - if (credentials.authType === 'clientsecret') { - const updated: AzureCredentials = { - ...credentials, - tenantId: event.target.value, - }; - onCredentialsChange(updated); - } - }; - - const onClientIdChange = (event: ChangeEvent) => { - if (credentials.authType === 'clientsecret') { - const updated: AzureCredentials = { - ...credentials, - clientId: event.target.value, - }; - onCredentialsChange(updated); - } - }; - - const onClientSecretChange = (event: ChangeEvent) => { - if (credentials.authType === 'clientsecret') { - const updated: AzureCredentials = { - ...credentials, - clientSecret: event.target.value, - }; - onCredentialsChange(updated); - } - }; - - const onClientSecretReset = () => { - if (credentials.authType === 'clientsecret') { - const updated: AzureCredentials = { - ...credentials, - clientSecret: '', - }; - onCredentialsChange(updated); - } - }; - - return ( - - {authTypeOptions.length > 1 && ( - - opt.value === credentials.azureCloud)} - options={azureCloudOptions} - onChange={onAzureCloudChange} - /> - - )} - - - - - - - {!disabled && - (typeof credentials.clientSecret === 'symbol' ? ( - -
- - -
-
- ) : ( - - - - ))} - - )} - {props.children} -
- ); -}; - -export default AzureCredentialsForm; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx new file mode 100644 index 00000000000..88ef5186df2 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx @@ -0,0 +1,149 @@ +import React, { ChangeEvent } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { Field, Select, Input, Button } from '@grafana/ui'; + +import { selectors } from '../../e2e/selectors'; +import { AzureClientSecretCredentials, AzureCredentials } from '../../types'; + +export interface AppRegistrationCredentialsProps { + credentials: AzureClientSecretCredentials; + azureCloudOptions?: SelectableValue[]; + onCredentialsChange: (updatedCredentials: AzureCredentials) => void; + disabled?: boolean; +} + +export const AppRegistrationCredentials = (props: AppRegistrationCredentialsProps) => { + const { azureCloudOptions, disabled, credentials, onCredentialsChange } = props; + + const onAzureCloudChange = (selected: SelectableValue) => { + const updated: AzureCredentials = { + ...credentials, + azureCloud: selected.value, + }; + onCredentialsChange(updated); + }; + + const onTenantIdChange = (event: ChangeEvent) => { + const updated: AzureCredentials = { + ...credentials, + tenantId: event.target.value, + }; + onCredentialsChange(updated); + }; + + const onClientIdChange = (event: ChangeEvent) => { + const updated: AzureCredentials = { + ...credentials, + clientId: event.target.value, + }; + onCredentialsChange(updated); + }; + + const onClientSecretChange = (event: ChangeEvent) => { + const updated: AzureCredentials = { + ...credentials, + clientSecret: event.target.value, + }; + onCredentialsChange(updated); + }; + + const onClientSecretReset = () => { + const updated: AzureCredentials = { + ...credentials, + clientSecret: '', + }; + onCredentialsChange(updated); + }; + + return ( + <> + {azureCloudOptions && ( + + + + + + + {!disabled && + (typeof credentials.clientSecret === 'symbol' ? ( + +
+ + +
+
+ ) : ( + + + + ))} + + ); +}; diff --git a/public/app/plugins/datasource/azuremonitor/components/AzureCredentialsForm.test.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.test.tsx similarity index 96% rename from public/app/plugins/datasource/azuremonitor/components/AzureCredentialsForm.test.tsx rename to public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.test.tsx index 4fed620eb4b..c94d96101b8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/AzureCredentialsForm.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.test.tsx @@ -7,6 +7,7 @@ const setup = (propsFunc?: (props: Props) => Props) => { let props: Props = { managedIdentityEnabled: false, workloadIdentityEnabled: false, + userIdentityEnabled: false, credentials: { authType: 'clientsecret', azureCloud: 'azuremonitor', @@ -14,7 +15,7 @@ const setup = (propsFunc?: (props: Props) => Props) => { clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900', clientSecret: undefined, }, - azureCloudOptions: [ + legacyAzureCloudOptions: [ { value: 'azuremonitor', label: 'Azure' }, { value: 'govazuremonitor', label: 'Azure US Government' }, { value: 'chinaazuremonitor', label: 'Azure China' }, diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx new file mode 100644 index 00000000000..232965f3d74 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx @@ -0,0 +1,126 @@ +import React, { useMemo } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { ConfigSection } from '@grafana/experimental'; +import { Select, Field } from '@grafana/ui'; + +import { selectors } from '../../e2e/selectors'; +import { AzureAuthType, AzureCredentials } from '../../types'; + +import { AppRegistrationCredentials } from './AppRegistrationCredentials'; +import CurrentUserFallbackCredentials from './CurrentUserFallbackCredentials'; + +export interface Props { + managedIdentityEnabled: boolean; + workloadIdentityEnabled: boolean; + userIdentityEnabled: boolean; + credentials: AzureCredentials; + azureCloudOptions?: SelectableValue[]; + legacyAzureCloudOptions?: SelectableValue[]; + onCredentialsChange: (updatedCredentials: AzureCredentials) => void; + disabled?: boolean; + children?: JSX.Element; +} + +export const AzureCredentialsForm = (props: Props) => { + const { + credentials, + azureCloudOptions, + legacyAzureCloudOptions, + onCredentialsChange, + disabled, + managedIdentityEnabled, + workloadIdentityEnabled, + userIdentityEnabled, + } = props; + + const authTypeOptions = useMemo(() => { + let opts: Array> = [ + { + value: 'clientsecret', + label: 'App Registration', + }, + ]; + + if (managedIdentityEnabled) { + opts.push({ + value: 'msi', + label: 'Managed Identity', + }); + } + + if (workloadIdentityEnabled) { + opts.push({ + value: 'workloadidentity', + label: 'Workload Identity', + }); + } + + if (userIdentityEnabled) { + opts.unshift({ + value: 'currentuser', + label: 'Current User', + }); + } + + return opts; + }, [managedIdentityEnabled, workloadIdentityEnabled, userIdentityEnabled]); + + const onAuthTypeChange = (selected: SelectableValue) => { + const defaultAuthType = managedIdentityEnabled + ? 'msi' + : workloadIdentityEnabled + ? 'workloadidentity' + : userIdentityEnabled + ? 'currentuser' + : 'clientsecret'; + const updated: AzureCredentials = { + ...credentials, + authType: selected.value || defaultAuthType, + }; + onCredentialsChange(updated); + }; + + return ( + + {authTypeOptions.length > 1 && ( + + opt.value === credentials.serviceCredentials?.authType)} + options={authTypeOptions} + onChange={onAuthTypeChange} + disabled={disabled} + /> + + )} + {credentials.serviceCredentials?.authType === 'clientsecret' && ( + + )} + + ) : null} + {props.children} + + ); +}; + +export default CurrentUserFallbackCredentials; diff --git a/public/app/plugins/datasource/azuremonitor/components/DefaultSubscription.test.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.test.tsx similarity index 94% rename from public/app/plugins/datasource/azuremonitor/components/DefaultSubscription.test.tsx rename to public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.test.tsx index 7fff646bb5d..9b5904684cd 100644 --- a/public/app/plugins/datasource/azuremonitor/components/DefaultSubscription.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.test.tsx @@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { createMockInstanceSetttings } from '../__mocks__/instanceSettings'; -import { selectors } from '../e2e/selectors'; +import { createMockInstanceSetttings } from '../../__mocks__/instanceSettings'; +import { selectors } from '../../e2e/selectors'; import { DefaultSubscription, Props } from './DefaultSubscription'; diff --git a/public/app/plugins/datasource/azuremonitor/components/DefaultSubscription.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx similarity index 94% rename from public/app/plugins/datasource/azuremonitor/components/DefaultSubscription.tsx rename to public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx index 939852ca876..c7bc2b33bf8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/DefaultSubscription.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx @@ -3,9 +3,9 @@ import React, { useEffect, useReducer } from 'react'; import { SelectableValue } from '@grafana/data'; import { Select, Button, Field } from '@grafana/ui'; -import { isCredentialsComplete } from '../credentials'; -import { selectors } from '../e2e/selectors'; -import { AzureCredentials, AzureDataSourceJsonData } from '../types'; +import { isCredentialsComplete } from '../../credentials'; +import { selectors } from '../../e2e/selectors'; +import { AzureCredentials, AzureDataSourceJsonData } from '../../types'; export interface Props { options: AzureDataSourceJsonData; diff --git a/public/app/plugins/datasource/azuremonitor/components/MonitorConfig.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx similarity index 82% rename from public/app/plugins/datasource/azuremonitor/components/MonitorConfig.tsx rename to public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx index 31d87064227..ff869479f9c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MonitorConfig.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx @@ -3,18 +3,25 @@ import React, { useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { getCredentials, updateCredentials } from '../credentials'; -import { AzureDataSourceSettings, AzureCredentials } from '../types'; +import { getCredentials, updateCredentials } from '../../credentials'; +import { AzureDataSourceSettings, AzureCredentials } from '../../types'; import { AzureCredentialsForm } from './AzureCredentialsForm'; import { DefaultSubscription } from './DefaultSubscription'; -const azureClouds: SelectableValue[] = [ +const legacyAzureClouds: SelectableValue[] = [ { value: 'azuremonitor', label: 'Azure' }, { value: 'govazuremonitor', label: 'Azure US Government' }, { value: 'chinaazuremonitor', label: 'Azure China' }, ]; +// This will be pulled from the azure-sdk in future +const azureClouds: SelectableValue[] = [ + { value: 'AzureCloud', label: 'Azure' }, + { value: 'AzureUSGovernment', label: 'Azure US Government' }, + { value: 'AzureChinaCloud', label: 'Azure China' }, +]; + export interface Props { options: AzureDataSourceSettings; updateOptions: (optionsFunc: (options: AzureDataSourceSettings) => AzureDataSourceSettings) => void; @@ -46,8 +53,10 @@ export const MonitorConfig = (props: Props) => { diff --git a/public/app/plugins/datasource/azuremonitor/components/AzureCheatSheet.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx similarity index 98% rename from public/app/plugins/datasource/azuremonitor/components/AzureCheatSheet.tsx rename to public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx index d517a4854e2..5bbe2b89937 100644 --- a/public/app/plugins/datasource/azuremonitor/components/AzureCheatSheet.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx @@ -15,7 +15,7 @@ import { useStyles2, } from '@grafana/ui'; -import AzureLogAnalyticsDatasource from '../azure_log_analytics/azure_log_analytics_datasource'; +import AzureLogAnalyticsDatasource from '../../azure_log_analytics/azure_log_analytics_datasource'; import { AzureMonitorQuery, AzureQueryType, @@ -23,7 +23,7 @@ import { CheatsheetQueries, CheatsheetQuery, DropdownCategories, -} from '../types'; +} from '../../types'; import { RawQuery } from './RawQuery'; import tokenizer from './syntax'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheetModal.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheetModal.tsx index a629823368a..3df12d38a8c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheetModal.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheetModal.tsx @@ -5,7 +5,8 @@ import { Modal } from '@grafana/ui'; import AzureLogAnalyticsDatasource from '../../azure_log_analytics/azure_log_analytics_datasource'; import { AzureMonitorQuery } from '../../dataquery.gen'; -import AzureCheatSheet from '../AzureCheatSheet'; + +import AzureCheatSheet from './AzureCheatSheet'; type Props = { isOpen: boolean; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 5bea85d8ea4..96a321c9e63 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -7,10 +7,10 @@ import { Alert, LinkButton } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat, EngineSchema } from '../../types'; -import FormatAsField from '../FormatAsField'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; import { parseResourceDetails } from '../ResourcePicker/utils'; +import FormatAsField from '../shared/FormatAsField'; import AdvancedResourcePicker from './AdvancedResourcePicker'; import QueryField from './QueryField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/RawQuery.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/RawQuery.tsx similarity index 100% rename from public/app/plugins/datasource/azuremonitor/components/RawQuery.tsx rename to public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/RawQuery.tsx diff --git a/public/app/plugins/datasource/azuremonitor/components/syntax.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/syntax.ts similarity index 100% rename from public/app/plugins/datasource/azuremonitor/components/syntax.ts rename to public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/syntax.ts diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx index 71981074a58..f464a8ae92b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx @@ -5,7 +5,7 @@ import { Select } from '@grafana/ui'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; import { addValueToOptions } from '../../utils/common'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { setAggregation } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx index 8e2d227ba05..68fde041f70 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx @@ -5,7 +5,7 @@ import { EditorList, AccessoryButton } from '@grafana/experimental'; import { Select, HorizontalGroup, MultiSelect } from '@grafana/ui'; import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { setDimensionFilters } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx index 1dbbb9b8c8a..142fea28167 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useState } from 'react'; import { Input } from '@grafana/ui'; import { AzureQueryEditorFieldProps } from '../../types'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { setLegendAlias } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx index a3f19d87bb0..e4d0fcd4ef3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx @@ -6,7 +6,7 @@ import { Select } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; import { addValueToOptions } from '../../utils/common'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { setMetricName } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx index c82bdfda94c..36abff0a15d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx @@ -5,7 +5,7 @@ import { Select } from '@grafana/ui'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; import { addValueToOptions } from '../../utils/common'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { setCustomNamespace } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx index 5e7cd0c8072..9739f1bd4cf 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx @@ -6,7 +6,7 @@ import { Select } from '@grafana/ui'; import TimegrainConverter from '../../time_grain_converter'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; import { addValueToOptions } from '../../utils/common'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { setTimeGrain } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx index e3f0254635b..3996b02fce3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useState } from 'react'; import { Input } from '@grafana/ui'; import { AzureQueryEditorFieldProps } from '../../types'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { setTop } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx index 629634f8e6e..fe200de0678 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx @@ -1,6 +1,8 @@ import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; +import { CoreApp } from '@grafana/data'; +import { config } from '@grafana/runtime'; import * as ui from '@grafana/ui'; import createMockDatasource from '../../__mocks__/datasource'; @@ -20,6 +22,11 @@ jest.mock('@grafana/ui', () => ({ }, })); +jest.mock('@grafana/runtime', () => ({ + ___esModule: true, + ...jest.requireActual('@grafana/runtime'), +})); + describe('Azure Monitor QueryEditor', () => { it('renders the Metrics query editor when the query type is Metrics', async () => { const mockDatasource = createMockDatasource(); @@ -121,4 +128,76 @@ describe('Azure Monitor QueryEditor', () => { expect(screen.getByTestId('data-testid azure-monitor-experimental-header')).toBeInTheDocument() ); }); + + it('renders the Metrics query editor when the data source is configured for user auth and the user is authenticated with Azure', async () => { + jest.mocked(config).bootData.user.authenticatedBy = 'oauth_azuread'; + const mockDatasource = createMockDatasource({ currentUserAuth: true }); + const mockQuery = { + ...createMockQuery(), + queryType: AzureQueryType.AzureMonitor, + }; + + render( {}} onRunQuery={() => {}} />); + await waitFor(() => + expect( + screen.getByTestId(selectors.components.queryEditor.metricsQueryEditor.container.input) + ).toBeInTheDocument() + ); + }); + + it('renders the user auth alert when the data source is configured for user auth and the user is not authenticated with Azure', async () => { + jest.mocked(config).bootData.user.authenticatedBy = 'not_azuread'; + const mockDatasource = createMockDatasource({ currentUserAuth: true }); + const mockQuery = { + ...createMockQuery(), + queryType: AzureQueryType.AzureMonitor, + }; + + render( {}} onRunQuery={() => {}} />); + await waitFor(() => expect(screen.getByTestId(selectors.components.queryEditor.userAuthAlert)).toBeInTheDocument()); + }); + + it('renders the user auth fallback alert when the data source is configured for user auth and fallback credentials are disabled', async () => { + jest.mocked(config).azure.userIdentityFallbackCredentialsEnabled = false; + const mockDatasource = createMockDatasource({ currentUserAuth: true }); + const mockQuery = { + ...createMockQuery(), + queryType: AzureQueryType.AzureMonitor, + }; + + render( + {}} + onRunQuery={() => {}} + /> + ); + await waitFor(() => + expect(screen.getByTestId(selectors.components.queryEditor.userAuthFallbackAlert)).toBeInTheDocument() + ); + }); + + it('renders the user auth fallback alert when the data source is configured for user auth and fallback credentials are enabled but the data source has none', async () => { + jest.mocked(config).azure.userIdentityFallbackCredentialsEnabled = true; + const mockDatasource = createMockDatasource({ currentUserAuth: true }); + const mockQuery = { + ...createMockQuery(), + queryType: AzureQueryType.AzureMonitor, + }; + + render( + {}} + onRunQuery={() => {}} + /> + ); + await waitFor(() => + expect(screen.getByTestId(selectors.components.queryEditor.userAuthFallbackAlert)).toBeInTheDocument() + ); + }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx index 8813d33f026..96239098982 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx @@ -2,11 +2,12 @@ import { css } from '@emotion/css'; import { debounce } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; -import { QueryEditorProps } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { CoreApp, QueryEditorProps } from '@grafana/data'; +import { config, reportInteraction } from '@grafana/runtime'; import { Alert, Button, CodeEditor, Space } from '@grafana/ui'; import AzureMonitorDatasource from '../../datasource'; +import { selectors } from '../../e2e/selectors'; import { AzureDataSourceJsonData, AzureMonitorErrorish, @@ -19,9 +20,9 @@ import ArgQueryEditor from '../ArgQueryEditor'; import LogsQueryEditor from '../LogsQueryEditor'; import { AzureCheatSheetModal } from '../LogsQueryEditor/AzureCheatSheetModal'; import NewMetricsQueryEditor from '../MetricsQueryEditor/MetricsQueryEditor'; -import { QueryHeader } from '../QueryHeader'; import TracesQueryEditor from '../TracesQueryEditor'; +import { QueryHeader } from './QueryHeader'; import usePreparedQuery from './usePreparedQuery'; export type AzureMonitorQueryEditorProps = QueryEditorProps< @@ -31,6 +32,7 @@ export type AzureMonitorQueryEditorProps = QueryEditorProps< >; const QueryEditor = ({ + app, query: baseQuery, datasource, onChange, @@ -58,6 +60,20 @@ const QueryEditor = ({ options: datasource.getVariables().map((v) => ({ label: v, value: v })), }; + const isAzureAuthenticated = config.bootData.user.authenticatedBy === 'oauth_azuread'; + + if (datasource.currentUserAuth) { + if ( + app === CoreApp.UnifiedAlerting && + (!config.azure.userIdentityFallbackCredentialsEnabled || !datasource.currentUserAuthFallbackAvailable) + ) { + return ; + } + if (!isAzureAuthenticated) { + return ; + } + } + return (
{type} was deprecated in Grafana 9. See the{' '} @@ -200,4 +216,45 @@ const EditorForQueryType = ({ } }; +const UserAuthAlert = () => { + return ( + + <> + Usage of this data source requires you to be authenticated via Azure Entra (formerly Azure Active Directory). + Please review the{' '} + + documentation + {' '} + for more information. + + + ); +}; + +const UserAuthFallbackAlert = () => { + return ( + + <> + Data source backend features (such as alerting) require service credentials to function. This data source is + configured without service credential fallback, or the fallback functionality is disabled. Please review the{' '} + + documentation + {' '} + for more information. + + + ); +}; + export default QueryEditor; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryHeader.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx similarity index 92% rename from public/app/plugins/datasource/azuremonitor/components/QueryHeader.tsx rename to public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx index fd2f60578bf..017d0c67670 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx @@ -3,8 +3,8 @@ import React, { useCallback } from 'react'; import { SelectableValue } from '@grafana/data'; import { EditorHeader, InlineSelect } from '@grafana/experimental'; -import { selectors } from '../e2e/selectors'; -import { AzureMonitorQuery, AzureQueryType } from '../types'; +import { selectors } from '../../e2e/selectors'; +import { AzureMonitorQuery, AzureQueryType } from '../../types'; interface QueryTypeFieldProps { query: AzureMonitorQuery; diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx index 7c23c2f15b0..af946a07dc5 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx @@ -7,11 +7,11 @@ import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData'; import { AzureQueryEditorFieldProps, AzureMonitorResource } from '../../types'; -import { Field } from '../Field'; import ResourcePicker from '../ResourcePicker'; import getStyles from '../ResourcePicker/styles'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; import { parseMultipleResourceDetails, setResources } from '../ResourcePicker/utils'; +import { Field } from '../shared/Field'; interface ResourceFieldProps extends AzureQueryEditorFieldProps { selectableEntryTypes: ResourceRowType[]; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx index bd4303b5d57..ffb602a7436 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx @@ -6,7 +6,7 @@ import { MultiSelect } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; import { findOptions } from '../../utils/common'; -import { Field } from '../Field'; +import { Field } from '../shared/Field'; import { Tables } from './consts'; import { setTraceTypes } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx index 2c295ece8e6..d4303efe819 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx @@ -8,12 +8,12 @@ import { Input } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat } from '../../types'; -import { Field } from '../Field'; -import FormatAsField from '../FormatAsField'; import AdvancedResourcePicker from '../LogsQueryEditor/AdvancedResourcePicker'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; import { parseResourceDetails } from '../ResourcePicker/utils'; +import { Field } from '../shared/Field'; +import FormatAsField from '../shared/FormatAsField'; import Filters from './Filters'; import TraceTypeField from './TraceTypeField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/Field.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx similarity index 100% rename from public/app/plugins/datasource/azuremonitor/components/Field.tsx rename to public/app/plugins/datasource/azuremonitor/components/shared/Field.tsx diff --git a/public/app/plugins/datasource/azuremonitor/components/FormatAsField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx similarity index 89% rename from public/app/plugins/datasource/azuremonitor/components/FormatAsField.test.tsx rename to public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx index 944643e46a5..9ed897b3634 100644 --- a/public/app/plugins/datasource/azuremonitor/components/FormatAsField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx @@ -1,12 +1,12 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import createMockDatasource from '../__mocks__/datasource'; -import createMockQuery from '../__mocks__/query'; -import { ResultFormat } from '../types'; +import createMockDatasource from '../../__mocks__/datasource'; +import createMockQuery from '../../__mocks__/query'; +import { ResultFormat } from '../../types'; +import { setFormatAs } from '../TracesQueryEditor/setQueryValue'; import FormatAsField from './FormatAsField'; -import { setFormatAs } from './TracesQueryEditor/setQueryValue'; const options = [ { label: 'Table', value: ResultFormat.Table }, diff --git a/public/app/plugins/datasource/azuremonitor/components/FormatAsField.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx similarity index 92% rename from public/app/plugins/datasource/azuremonitor/components/FormatAsField.tsx rename to public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx index 884edb0ec0b..ededa624ce8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/FormatAsField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx @@ -4,8 +4,8 @@ import { useEffectOnce } from 'react-use'; import { SelectableValue } from '@grafana/data'; import { Select } from '@grafana/ui'; -import { selectors } from '../e2e/selectors'; -import { FormatAsFieldProps, ResultFormat } from '../types'; +import { selectors } from '../../e2e/selectors'; +import { FormatAsFieldProps, ResultFormat } from '../../types'; import { Field } from './Field'; diff --git a/public/app/plugins/datasource/azuremonitor/credentials.ts b/public/app/plugins/datasource/azuremonitor/credentials.ts index 25ec5a4aa98..48797303301 100644 --- a/public/app/plugins/datasource/azuremonitor/credentials.ts +++ b/public/app/plugins/datasource/azuremonitor/credentials.ts @@ -1,7 +1,9 @@ import { config } from '@grafana/runtime'; import { + AadCurrentUserCredentials, AzureAuthType, + AzureClientSecretCredentials, AzureCloud, AzureCredentials, AzureDataSourceInstanceSettings, @@ -63,6 +65,7 @@ export function getAzureCloud(options: AzureDataSourceSettings | AzureDataSource // In case of managed identity and workload identity, the cloud is always same as where Grafana is hosted return getDefaultAzureCloud(); case 'clientsecret': + case 'currentuser': return options.jsonData.cloudName || getDefaultAzureCloud(); } } @@ -77,18 +80,36 @@ function getSecret(options: AzureDataSourceSettings): undefined | string | Conce } } -export function isCredentialsComplete(credentials: AzureCredentials): boolean { +export function isCredentialsComplete(credentials: AzureCredentials, ignoreSecret = false): boolean { switch (credentials.authType) { case 'msi': case 'workloadidentity': + case 'currentuser': return true; case 'clientsecret': - return !!(credentials.azureCloud && credentials.tenantId && credentials.clientId && credentials.clientSecret); + return !!( + credentials.azureCloud && + credentials.tenantId && + credentials.clientId && + // When ignoreSecret is set we consider the credentials complete without checking the secret + !!(ignoreSecret || credentials.clientSecret) + ); } } +export function instanceOfAzureCredential( + authType: AzureAuthType, + object?: AzureCredentials +): object is T { + if (!object) { + return false; + } + return object.authType === authType; +} + export function getCredentials(options: AzureDataSourceSettings): AzureCredentials { const authType = getAuthType(options); + const credentials = options.jsonData.azureCredentials; switch (authType) { case 'msi': case 'workloadidentity': @@ -109,13 +130,32 @@ export function getCredentials(options: AzureDataSourceSettings): AzureCredentia } case 'clientsecret': return { - authType: 'clientsecret', + authType, azureCloud: options.jsonData.cloudName || getDefaultAzureCloud(), tenantId: options.jsonData.tenantId, clientId: options.jsonData.clientId, clientSecret: getSecret(options), }; } + if (instanceOfAzureCredential(authType, credentials)) { + if (instanceOfAzureCredential('clientsecret', credentials.serviceCredentials)) { + const serviceCredentials = { ...credentials.serviceCredentials, clientSecret: getSecret(options) }; + return { + authType, + serviceCredentialsEnabled: credentials.serviceCredentialsEnabled, + serviceCredentials, + }; + } + return { + authType, + serviceCredentialsEnabled: credentials.serviceCredentialsEnabled, + serviceCredentials: credentials.serviceCredentials, + }; + } + return { + authType: 'clientsecret', + azureCloud: getDefaultAzureCloud(), + }; } export function updateCredentials( @@ -137,6 +177,7 @@ export function updateCredentials( jsonData: { ...options.jsonData, azureAuthType: credentials.authType, + azureCredentials: undefined, }, }; @@ -147,10 +188,11 @@ export function updateCredentials( ...options, jsonData: { ...options.jsonData, - azureAuthType: 'clientsecret', + azureAuthType: credentials.authType, cloudName: credentials.azureCloud || getDefaultAzureCloud(), tenantId: credentials.tenantId, clientId: credentials.clientId, + azureCredentials: undefined, }, secureJsonData: { ...options.secureJsonData, @@ -161,7 +203,37 @@ export function updateCredentials( clientSecret: typeof credentials.clientSecret === 'symbol', }, }; - - return options; } + if (instanceOfAzureCredential('currentuser', credentials)) { + const serviceCredentials = credentials.serviceCredentials; + let clientSecret: string | symbol | undefined; + if (instanceOfAzureCredential('clientsecret', serviceCredentials)) { + clientSecret = serviceCredentials.clientSecret; + // Do this to not expose the secret in unencrypted JSON data + delete serviceCredentials.clientSecret; + } + options = { + ...options, + jsonData: { + ...options.jsonData, + azureAuthType: credentials.authType, + azureCredentials: { + authType: 'currentuser', + serviceCredentialsEnabled: credentials.serviceCredentialsEnabled, + serviceCredentials, + }, + oauthPassThru: true, + disableGrafanaCache: true, + }, + secureJsonData: { + ...options.secureJsonData, + clientSecret: typeof clientSecret === 'string' ? clientSecret : undefined, + }, + secureJsonFields: { + ...options.secureJsonFields, + clientSecret: typeof clientSecret === 'symbol', + }, + }; + } + return options; } diff --git a/public/app/plugins/datasource/azuremonitor/datasource.ts b/public/app/plugins/datasource/azuremonitor/datasource.ts index 964b958ded9..9e785df76d8 100644 --- a/public/app/plugins/datasource/azuremonitor/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/datasource.ts @@ -16,8 +16,9 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource'; import AzureResourceGraphDatasource from './azure_resource_graph/azure_resource_graph_datasource'; +import { instanceOfAzureCredential, isCredentialsComplete } from './credentials'; import ResourcePickerData from './resourcePicker/resourcePickerData'; -import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from './types'; +import { AadCurrentUserCredentials, AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from './types'; import migrateAnnotation from './utils/migrateAnnotation'; import migrateQuery from './utils/migrateQuery'; import { VariableSupport } from './variables'; @@ -31,6 +32,8 @@ export default class Datasource extends DataSourceWithBackend('currentuser', credentials)) { + if (!credentials.serviceCredentials) { + this.currentUserAuthFallbackAvailable = false; + } else { + this.currentUserAuthFallbackAvailable = isCredentialsComplete(credentials.serviceCredentials, true); + } + } else { + this.currentUserAuthFallbackAvailable = false; + } } filterQuery(item: AzureMonitorQuery): boolean { diff --git a/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts b/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts index 9d77b7f927c..e2c8c09bb73 100644 --- a/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts +++ b/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts @@ -23,11 +23,16 @@ export const components = { defaultSubscription: { input: 'data-testid default-subscription', }, + serviceCredentialsEnabled: { + button: 'data-testid service-credentials-enabled', + }, }, queryEditor: { header: { select: 'data-testid azure-monitor-experimental-header', }, + userAuthAlert: 'data-testid azure-monitor-user-auth-invalid-auth-provider-alert', + userAuthFallbackAlert: 'data-testid azure-monitor-user-auth-fallback-alert', resourcePicker: { select: { button: 'data-testid resource-picker-select', diff --git a/public/app/plugins/datasource/azuremonitor/module.ts b/public/app/plugins/datasource/azuremonitor/module.ts index 6322011442d..04264961804 100644 --- a/public/app/plugins/datasource/azuremonitor/module.ts +++ b/public/app/plugins/datasource/azuremonitor/module.ts @@ -1,7 +1,7 @@ import { DataSourcePlugin, DashboardLoadedEvent } from '@grafana/data'; import { getAppEvents } from '@grafana/runtime'; -import { ConfigEditor } from './components/ConfigEditor'; +import { ConfigEditor } from './components/ConfigEditor/ConfigEditor'; import AzureMonitorQueryEditor from './components/QueryEditor'; import Datasource from './datasource'; import pluginJson from './plugin.json'; diff --git a/public/app/plugins/datasource/azuremonitor/types/types.ts b/public/app/plugins/datasource/azuremonitor/types/types.ts index b0372b75724..6f1c3286f2a 100644 --- a/public/app/plugins/datasource/azuremonitor/types/types.ts +++ b/public/app/plugins/datasource/azuremonitor/types/types.ts @@ -33,7 +33,7 @@ export enum AzureCloud { None = '', } -export type AzureAuthType = 'msi' | 'clientsecret' | 'workloadidentity'; +export type AzureAuthType = 'msi' | 'clientsecret' | 'workloadidentity' | 'currentuser'; export type ConcealedSecret = symbol; @@ -56,8 +56,17 @@ export interface AzureClientSecretCredentials extends AzureCredentialsBase { clientId?: string; clientSecret?: string | ConcealedSecret; } +export interface AadCurrentUserCredentials extends AzureCredentialsBase { + authType: 'currentuser'; + serviceCredentials?: + | AzureClientSecretCredentials + | AzureManagedIdentityCredentials + | AzureWorkloadIdentityCredentials; + serviceCredentialsEnabled?: boolean; +} export type AzureCredentials = + | AadCurrentUserCredentials | AzureManagedIdentityCredentials | AzureClientSecretCredentials | AzureWorkloadIdentityCredentials; @@ -70,6 +79,8 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData { tenantId?: string; clientId?: string; subscriptionId?: string; + oauthPassThru?: boolean; + azureCredentials?: AzureCredentials; // logs /** @deprecated Azure Logs credentials */ diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts index bf3b00063ff..42daca490ef 100644 --- a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts +++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts @@ -4,7 +4,12 @@ import { GrafanaBootConfig } from '@grafana/runtime'; import { AzureAuthSecureJSONDataType, AzureAuthJSONDataType, AzureAuthType } from '../types'; export const configWithManagedIdentityEnabled: Partial = { - azure: { managedIdentityEnabled: true, workloadIdentityEnabled: false, userIdentityEnabled: false }, + azure: { + managedIdentityEnabled: true, + workloadIdentityEnabled: false, + userIdentityEnabled: false, + userIdentityFallbackCredentialsEnabled: false, + }, }; export const configWithManagedIdentityDisabled: Partial = { @@ -13,6 +18,7 @@ export const configWithManagedIdentityDisabled: Partial = { workloadIdentityEnabled: false, userIdentityEnabled: false, cloud: 'AzureCloud', + userIdentityFallbackCredentialsEnabled: false, }, };