mirror of
https://github.com/grafana/grafana.git
synced 2024-11-21 16:38:03 -06:00
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>
This commit is contained in:
parent
2a6a1fb3b3
commit
6bb7ab261a
@ -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 =
|
||||
|
@ -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 =
|
||||
|
@ -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: <subscription-id> # 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/<GRAFANA VERSION>/setup-grafana/configure-grafana#azure"
|
||||
[configure-grafana-azure]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana#azure"
|
||||
|
||||
[configure-grafana-azure-auth]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-security/configure-authentication/azuread"
|
||||
[configure-grafana-azure-auth]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-security/configure-authentication/azuread"
|
||||
|
||||
[configure-grafana-azure-auth-scopes]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-security/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana"
|
||||
[configure-grafana-azure-auth-scopes]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-security/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana"
|
||||
|
||||
[configure-grafana-feature-toggles]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana/#feature_toggles"
|
||||
[configure-grafana-feature-toggles]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/setup-grafana/configure-grafana/#feature_toggles"
|
||||
|
||||
[data-source-management]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/administration/data-source-management"
|
||||
[data-source-management]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/administration/data-source-management"
|
||||
|
||||
|
@ -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.
|
||||
|
@ -126,6 +126,7 @@ export interface CurrentUserDTO {
|
||||
language: string;
|
||||
permissions?: Record<string, boolean>;
|
||||
analytics: AnalyticsSettings;
|
||||
authenticatedBy: string;
|
||||
|
||||
/** @deprecated Use theme instead */
|
||||
lightTheme: boolean;
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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 != "" {
|
||||
|
@ -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",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
|
||||
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
getVariablesRaw: jest.fn().mockReturnValue([]),
|
||||
currentUserAuth: false,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<SelectableValue<AzureAuthType>> = [
|
||||
{
|
||||
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<AzureAuthType>) => {
|
||||
const defaultAuthType = managedIdentityEnabled
|
||||
? 'msi'
|
||||
: workloadIdentityEnabled
|
||||
? 'workloadidentity'
|
||||
: 'clientsecret';
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
authType: selected.value || defaultAuthType,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onAzureCloudChange = (selected: SelectableValue<string>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
azureCloud: selected.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onTenantIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
tenantId: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onClientIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientId: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onClientSecretChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<ConfigSection title="Authentication">
|
||||
{authTypeOptions.length > 1 && (
|
||||
<Field
|
||||
label="Authentication"
|
||||
description="Choose the type of authentication to Azure services"
|
||||
data-testid={selectors.components.configEditor.authType.select}
|
||||
htmlFor="authentication-type"
|
||||
>
|
||||
<Select
|
||||
className="width-15"
|
||||
value={authTypeOptions.find((opt) => opt.value === credentials.authType)}
|
||||
options={authTypeOptions}
|
||||
onChange={onAuthTypeChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{credentials.authType === 'clientsecret' && (
|
||||
<>
|
||||
{azureCloudOptions && (
|
||||
<Field
|
||||
label="Azure Cloud"
|
||||
data-testid={selectors.components.configEditor.azureCloud.input}
|
||||
htmlFor="azure-cloud-type"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Select
|
||||
inputId="azure-cloud-type"
|
||||
aria-label="Azure Cloud"
|
||||
className="width-15"
|
||||
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
|
||||
options={azureCloudOptions}
|
||||
onChange={onAzureCloudChange}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Field
|
||||
label="Directory (tenant) ID"
|
||||
required
|
||||
data-testid={selectors.components.configEditor.tenantID.input}
|
||||
htmlFor="tenant-id"
|
||||
invalid={!credentials.tenantId}
|
||||
error={'Tenant ID is required'}
|
||||
>
|
||||
<Input
|
||||
aria-label="Tenant ID"
|
||||
className="width-30"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.tenantId || ''}
|
||||
onChange={onTenantIdChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Application (client) ID"
|
||||
required
|
||||
data-testid={selectors.components.configEditor.clientID.input}
|
||||
htmlFor="client-id"
|
||||
invalid={!credentials.clientId}
|
||||
error={'Client ID is required'}
|
||||
>
|
||||
<Input
|
||||
className="width-30"
|
||||
aria-label="Client ID"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientId || ''}
|
||||
onChange={onClientIdChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
{!disabled &&
|
||||
(typeof credentials.clientSecret === 'symbol' ? (
|
||||
<Field label="Client Secret" htmlFor="client-secret" required>
|
||||
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
|
||||
<Input
|
||||
aria-label="Client Secret"
|
||||
placeholder="configured"
|
||||
disabled={true}
|
||||
data-testid={'client-secret'}
|
||||
/>
|
||||
<Button variant="secondary" type="button" onClick={onClientSecretReset} disabled={disabled}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
) : (
|
||||
<Field
|
||||
label="Client Secret"
|
||||
data-testid={selectors.components.configEditor.clientSecret.input}
|
||||
required
|
||||
htmlFor="client-secret"
|
||||
invalid={!credentials.clientSecret}
|
||||
error={'Client secret is required'}
|
||||
>
|
||||
<Input
|
||||
className="width-30"
|
||||
aria-label="Client Secret"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientSecret || ''}
|
||||
onChange={onClientSecretChange}
|
||||
id="client-secret"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{props.children}
|
||||
</ConfigSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default AzureCredentialsForm;
|
@ -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<string>) => {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
azureCloud: selected.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onTenantIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
tenantId: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onClientIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientId: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onClientSecretChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onClientSecretReset = () => {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: '',
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{azureCloudOptions && (
|
||||
<Field
|
||||
label="Azure Cloud"
|
||||
data-testid={selectors.components.configEditor.azureCloud.input}
|
||||
htmlFor="azure-cloud-type"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Select
|
||||
inputId="azure-cloud-type"
|
||||
aria-label="Azure Cloud"
|
||||
className="width-15"
|
||||
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
|
||||
options={azureCloudOptions}
|
||||
onChange={onAzureCloudChange}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Field
|
||||
label="Directory (tenant) ID"
|
||||
required={credentials.authType === 'clientsecret'}
|
||||
data-testid={selectors.components.configEditor.tenantID.input}
|
||||
htmlFor="tenant-id"
|
||||
invalid={credentials.authType === 'clientsecret' && !credentials.tenantId}
|
||||
error={'Tenant ID is required'}
|
||||
>
|
||||
<Input
|
||||
aria-label="Tenant ID"
|
||||
className="width-30"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.tenantId || ''}
|
||||
onChange={onTenantIdChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Application (client) ID"
|
||||
required={credentials.authType === 'clientsecret'}
|
||||
data-testid={selectors.components.configEditor.clientID.input}
|
||||
htmlFor="client-id"
|
||||
invalid={credentials.authType === 'clientsecret' && !credentials.clientId}
|
||||
error={'Client ID is required'}
|
||||
>
|
||||
<Input
|
||||
className="width-30"
|
||||
aria-label="Client ID"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientId || ''}
|
||||
onChange={onClientIdChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
{!disabled &&
|
||||
(typeof credentials.clientSecret === 'symbol' ? (
|
||||
<Field label="Client Secret" htmlFor="client-secret" required>
|
||||
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
|
||||
<Input
|
||||
aria-label="Client Secret"
|
||||
placeholder="configured"
|
||||
disabled={true}
|
||||
data-testid={'client-secret'}
|
||||
/>
|
||||
<Button variant="secondary" type="button" onClick={onClientSecretReset} disabled={disabled}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
) : (
|
||||
<Field
|
||||
label="Client Secret"
|
||||
data-testid={selectors.components.configEditor.clientSecret.input}
|
||||
required
|
||||
htmlFor="client-secret"
|
||||
invalid={!credentials.clientSecret}
|
||||
error={'Client secret is required'}
|
||||
>
|
||||
<Input
|
||||
className="width-30"
|
||||
aria-label="Client Secret"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientSecret || ''}
|
||||
onChange={onClientSecretChange}
|
||||
id="client-secret"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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' },
|
@ -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<SelectableValue<AzureAuthType>> = [
|
||||
{
|
||||
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<AzureAuthType>) => {
|
||||
const defaultAuthType = managedIdentityEnabled
|
||||
? 'msi'
|
||||
: workloadIdentityEnabled
|
||||
? 'workloadidentity'
|
||||
: userIdentityEnabled
|
||||
? 'currentuser'
|
||||
: 'clientsecret';
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
authType: selected.value || defaultAuthType,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigSection title="Authentication">
|
||||
{authTypeOptions.length > 1 && (
|
||||
<Field
|
||||
label="Authentication"
|
||||
description="Choose the type of authentication to Azure services"
|
||||
data-testid={selectors.components.configEditor.authType.select}
|
||||
htmlFor="authentication-type"
|
||||
>
|
||||
<Select
|
||||
className="width-15"
|
||||
value={authTypeOptions.find((opt) => opt.value === credentials.authType)}
|
||||
options={authTypeOptions}
|
||||
onChange={onAuthTypeChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{credentials.authType === 'clientsecret' && (
|
||||
<AppRegistrationCredentials
|
||||
credentials={credentials}
|
||||
azureCloudOptions={legacyAzureCloudOptions}
|
||||
onCredentialsChange={onCredentialsChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{props.children}
|
||||
{credentials.authType === 'currentuser' && (
|
||||
<CurrentUserFallbackCredentials
|
||||
credentials={credentials}
|
||||
azureCloudOptions={azureCloudOptions}
|
||||
onCredentialsChange={onCredentialsChange}
|
||||
disabled={disabled}
|
||||
managedIdentityEnabled={managedIdentityEnabled}
|
||||
workloadIdentityEnabled={workloadIdentityEnabled}
|
||||
userIdentityEnabled={userIdentityEnabled}
|
||||
/>
|
||||
)}
|
||||
</ConfigSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default AzureCredentialsForm;
|
@ -5,15 +5,15 @@ import { ConfigSection, DataSourceDescription } from '@grafana/experimental';
|
||||
import { getBackendSrv, getTemplateSrv, isFetchError, TemplateSrv, config } from '@grafana/runtime';
|
||||
import { Alert, Divider, SecureSocksProxySettings } from '@grafana/ui';
|
||||
|
||||
import ResponseParser from '../azure_monitor/response_parser';
|
||||
import ResponseParser from '../../azure_monitor/response_parser';
|
||||
import {
|
||||
AzureAPIResponse,
|
||||
AzureDataSourceJsonData,
|
||||
AzureDataSourceSecureJsonData,
|
||||
AzureDataSourceSettings,
|
||||
Subscription,
|
||||
} from '../types';
|
||||
import { routeNames } from '../utils/common';
|
||||
} from '../../types';
|
||||
import { routeNames } from '../../utils/common';
|
||||
|
||||
import { MonitorConfig } from './MonitorConfig';
|
||||
|
@ -0,0 +1,74 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { selectors } from '../../e2e/selectors';
|
||||
|
||||
import CurrentUserFallbackCredentials, { Props } from './CurrentUserFallbackCredentials';
|
||||
|
||||
const setup = (propsFunc?: (props: Props) => Props) => {
|
||||
let props: Props = {
|
||||
managedIdentityEnabled: true,
|
||||
workloadIdentityEnabled: true,
|
||||
userIdentityEnabled: true,
|
||||
credentials: { authType: 'currentuser' },
|
||||
azureCloudOptions: [
|
||||
{ value: 'AzureCloud', label: 'Azure' },
|
||||
{ value: 'AzureUSGovernment', label: 'Azure US Government' },
|
||||
{ value: 'AzureChinaCloud', label: 'Azure China' },
|
||||
],
|
||||
onCredentialsChange: jest.fn(),
|
||||
};
|
||||
|
||||
if (propsFunc) {
|
||||
props = propsFunc(props);
|
||||
}
|
||||
|
||||
return { ...render(<CurrentUserFallbackCredentials {...props} />), props };
|
||||
};
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
___esModule: true,
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
}));
|
||||
|
||||
describe('CurrentUserFallbackCredentials', () => {
|
||||
it('should render alert if fallback credentials disabled', async () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByText('Fallback Credentials Disabled')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render component', async () => {
|
||||
jest.mocked(config).azure.userIdentityFallbackCredentialsEnabled = true;
|
||||
setup();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByTestId(selectors.components.configEditor.serviceCredentialsEnabled.button)
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should enable service credentials', async () => {
|
||||
jest.mocked(config).azure.userIdentityFallbackCredentialsEnabled = true;
|
||||
const onCredentialsChange = jest.fn();
|
||||
const { rerender, props } = setup((props) => ({
|
||||
...props,
|
||||
onCredentialsChange,
|
||||
}));
|
||||
|
||||
await waitFor(() => fireEvent.click(screen.getByLabelText('Enabled')));
|
||||
expect(onCredentialsChange).toHaveBeenCalled();
|
||||
expect(onCredentialsChange).toHaveBeenCalledWith({ authType: 'currentuser', serviceCredentialsEnabled: true });
|
||||
|
||||
rerender(
|
||||
<CurrentUserFallbackCredentials
|
||||
{...props}
|
||||
credentials={{ ...props.credentials, serviceCredentialsEnabled: true }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Authentication')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,178 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { ConfigSection } from '@grafana/experimental';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Select, Field, RadioButtonGroup, Alert, Stack } from '@grafana/ui';
|
||||
|
||||
import { instanceOfAzureCredential } from '../../credentials';
|
||||
import { selectors } from '../../e2e/selectors';
|
||||
import { AadCurrentUserCredentials, AzureAuthType, AzureCredentials } from '../../types';
|
||||
|
||||
import { AppRegistrationCredentials } from './AppRegistrationCredentials';
|
||||
|
||||
export interface Props {
|
||||
managedIdentityEnabled: boolean;
|
||||
workloadIdentityEnabled: boolean;
|
||||
userIdentityEnabled: boolean;
|
||||
credentials: AadCurrentUserCredentials;
|
||||
azureCloudOptions?: SelectableValue[];
|
||||
onCredentialsChange: (updatedCredentials: AzureCredentials) => void;
|
||||
disabled?: boolean;
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
export const CurrentUserFallbackCredentials = (props: Props) => {
|
||||
const {
|
||||
credentials,
|
||||
azureCloudOptions,
|
||||
onCredentialsChange,
|
||||
disabled,
|
||||
managedIdentityEnabled,
|
||||
workloadIdentityEnabled,
|
||||
} = props;
|
||||
|
||||
const authTypeOptions = useMemo(() => {
|
||||
let opts: Array<SelectableValue<Exclude<AzureAuthType, 'currentuser'>>> = [
|
||||
{
|
||||
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<Exclude<AzureAuthType, 'currentuser'>>) => {
|
||||
const defaultAuthType = managedIdentityEnabled
|
||||
? 'msi'
|
||||
: workloadIdentityEnabled
|
||||
? 'workloadidentity'
|
||||
: 'clientsecret';
|
||||
const updated: AadCurrentUserCredentials = {
|
||||
...credentials,
|
||||
serviceCredentials: {
|
||||
authType: selected.value || defaultAuthType,
|
||||
},
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onServiceCredentialsEnabledChange = (value: boolean) => {
|
||||
let updated: AzureCredentials = {
|
||||
...credentials,
|
||||
serviceCredentialsEnabled: value,
|
||||
};
|
||||
if (!value) {
|
||||
updated = { ...updated, serviceCredentials: undefined };
|
||||
}
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onServiceCredentialsChange = (serviceCredentials: AzureCredentials) => {
|
||||
if (!instanceOfAzureCredential('currentuser', serviceCredentials)) {
|
||||
onCredentialsChange({ ...credentials, serviceCredentials: serviceCredentials });
|
||||
}
|
||||
};
|
||||
|
||||
if (!config.azure.userIdentityFallbackCredentialsEnabled) {
|
||||
return (
|
||||
<Alert severity="info" title="Fallback Credentials Disabled">
|
||||
<>
|
||||
Fallback credentials have been disabled. As user-based authentication only inherently supports requests with a
|
||||
user in scope, features such as alerting, recorded queries, or reporting will not function as expected. Please
|
||||
review the{' '}
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/azuremonitor/deprecated-application-insights/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
for more details.
|
||||
</>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigSection title="Fallback Service Credentials" isCollapsible={true}>
|
||||
<Alert severity="info" title="Service Credentials">
|
||||
<Stack direction={'column'}>
|
||||
<div>
|
||||
User-based authentication does not inherently support Grafana features that make requests to the data source
|
||||
without a users details available to the request. An example of this is alerting. If you wish to ensure that
|
||||
features that do not have a user in the context of the request still function, please provide fallback
|
||||
credentials below.
|
||||
</div>
|
||||
<div>
|
||||
<b>
|
||||
Note: Features like alerting will be restricted to the access level of the fallback credentials rather
|
||||
than the user. This may present confusion for users and should be clarified.
|
||||
</b>
|
||||
</div>
|
||||
</Stack>
|
||||
</Alert>
|
||||
<Field
|
||||
label="Service Credentials"
|
||||
description="Choose if fallback service credentials are enabled or disabled for this data source"
|
||||
data-testid={selectors.components.configEditor.serviceCredentialsEnabled.button}
|
||||
>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'Enabled', value: true },
|
||||
{ label: 'Disabled', value: false },
|
||||
]}
|
||||
value={credentials.serviceCredentialsEnabled ?? false}
|
||||
size={'md'}
|
||||
onChange={(val) => onServiceCredentialsEnabledChange(val)}
|
||||
/>
|
||||
</Field>
|
||||
{credentials.serviceCredentialsEnabled ? (
|
||||
<>
|
||||
{authTypeOptions.length > 0 && (
|
||||
<Field
|
||||
label="Authentication"
|
||||
description="Choose the type of authentication to Azure services"
|
||||
data-testid={selectors.components.configEditor.authType.select}
|
||||
htmlFor="authentication-type"
|
||||
>
|
||||
<Select
|
||||
className="width-15"
|
||||
value={authTypeOptions.find((opt) => opt.value === credentials.serviceCredentials?.authType)}
|
||||
options={authTypeOptions}
|
||||
onChange={onAuthTypeChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{credentials.serviceCredentials?.authType === 'clientsecret' && (
|
||||
<AppRegistrationCredentials
|
||||
credentials={credentials.serviceCredentials}
|
||||
azureCloudOptions={azureCloudOptions}
|
||||
onCredentialsChange={onServiceCredentialsChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{props.children}
|
||||
</ConfigSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentUserFallbackCredentials;
|
@ -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';
|
||||
|
@ -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;
|
@ -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) => {
|
||||
<AzureCredentialsForm
|
||||
managedIdentityEnabled={config.azure.managedIdentityEnabled}
|
||||
workloadIdentityEnabled={config.azure.workloadIdentityEnabled}
|
||||
userIdentityEnabled={config.azure.userIdentityEnabled}
|
||||
credentials={credentials}
|
||||
azureCloudOptions={azureClouds}
|
||||
legacyAzureCloudOptions={legacyAzureClouds}
|
||||
onCredentialsChange={onCredentialsChange}
|
||||
disabled={props.options.readOnly}
|
||||
>
|
@ -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';
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} 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(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} 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(
|
||||
<QueryEditor
|
||||
app={CoreApp.UnifiedAlerting}
|
||||
query={mockQuery}
|
||||
datasource={mockDatasource}
|
||||
onChange={() => {}}
|
||||
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(
|
||||
<QueryEditor
|
||||
app={CoreApp.UnifiedAlerting}
|
||||
query={mockQuery}
|
||||
datasource={mockDatasource}
|
||||
onChange={() => {}}
|
||||
onRunQuery={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(selectors.components.queryEditor.userAuthFallbackAlert)).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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 <UserAuthFallbackAlert />;
|
||||
}
|
||||
if (!isAzureAuthenticated) {
|
||||
return <UserAuthAlert />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="azure-monitor-query-editor">
|
||||
<AzureCheatSheetModal
|
||||
@ -185,7 +201,7 @@ const EditorForQueryType = ({
|
||||
<>
|
||||
{type} was deprecated in Grafana 9. See the{' '}
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/azuremonitor/deprecated-application-insights/"
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/azure-monitor/#application-insights-and-insights-analytics-removed"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@ -200,4 +216,45 @@ const EditorForQueryType = ({
|
||||
}
|
||||
};
|
||||
|
||||
const UserAuthAlert = () => {
|
||||
return (
|
||||
<Alert title="Unsupported authentication provider" data-testid={selectors.components.queryEditor.userAuthAlert}>
|
||||
<>
|
||||
Usage of this data source requires you to be authenticated via Azure Entra (formerly Azure Active Directory).
|
||||
Please review the{' '}
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/azure-monitor/#configure-current-user-authentication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
const UserAuthFallbackAlert = () => {
|
||||
return (
|
||||
<Alert
|
||||
title="No fallback credentials available"
|
||||
data-testid={selectors.components.queryEditor.userAuthFallbackAlert}
|
||||
>
|
||||
<>
|
||||
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{' '}
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/azure-monitor/#configure-current-user-authentication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryEditor;
|
||||
|
@ -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;
|
@ -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<T> extends AzureQueryEditorFieldProps {
|
||||
selectableEntryTypes: ResourceRowType[];
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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 },
|
@ -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';
|
||||
|
@ -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<T extends AzureCredentials>(
|
||||
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<AadCurrentUserCredentials>(authType, credentials)) {
|
||||
if (instanceOfAzureCredential<AzureClientSecretCredentials>('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<AadCurrentUserCredentials>('currentuser', credentials)) {
|
||||
const serviceCredentials = credentials.serviceCredentials;
|
||||
let clientSecret: string | symbol | undefined;
|
||||
if (instanceOfAzureCredential<AzureClientSecretCredentials>('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;
|
||||
}
|
||||
|
@ -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<AzureMonitorQuery,
|
||||
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
|
||||
resourcePickerData: ResourcePickerData;
|
||||
azureResourceGraphDatasource: AzureResourceGraphDatasource;
|
||||
currentUserAuth: boolean;
|
||||
currentUserAuthFallbackAvailable: boolean;
|
||||
|
||||
pseudoDatasource: {
|
||||
[key in AzureQueryType]?: AzureMonitorDatasource | AzureLogAnalyticsDatasource | AzureResourceGraphDatasource;
|
||||
@ -55,6 +58,18 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
|
||||
};
|
||||
|
||||
this.variables = new VariableSupport(this);
|
||||
|
||||
this.currentUserAuth = instanceSettings.jsonData.azureAuthType === 'currentuser';
|
||||
const credentials = instanceSettings.jsonData.azureCredentials;
|
||||
if (credentials && instanceOfAzureCredential<AadCurrentUserCredentials>('currentuser', credentials)) {
|
||||
if (!credentials.serviceCredentials) {
|
||||
this.currentUserAuthFallbackAvailable = false;
|
||||
} else {
|
||||
this.currentUserAuthFallbackAvailable = isCredentialsComplete(credentials.serviceCredentials, true);
|
||||
}
|
||||
} else {
|
||||
this.currentUserAuthFallbackAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
filterQuery(item: AzureMonitorQuery): boolean {
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
|
@ -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 */
|
||||
|
@ -4,7 +4,12 @@ import { GrafanaBootConfig } from '@grafana/runtime';
|
||||
import { AzureAuthSecureJSONDataType, AzureAuthJSONDataType, AzureAuthType } from '../types';
|
||||
|
||||
export const configWithManagedIdentityEnabled: Partial<GrafanaBootConfig> = {
|
||||
azure: { managedIdentityEnabled: true, workloadIdentityEnabled: false, userIdentityEnabled: false },
|
||||
azure: {
|
||||
managedIdentityEnabled: true,
|
||||
workloadIdentityEnabled: false,
|
||||
userIdentityEnabled: false,
|
||||
userIdentityFallbackCredentialsEnabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const configWithManagedIdentityDisabled: Partial<GrafanaBootConfig> = {
|
||||
@ -13,6 +18,7 @@ export const configWithManagedIdentityDisabled: Partial<GrafanaBootConfig> = {
|
||||
workloadIdentityEnabled: false,
|
||||
userIdentityEnabled: false,
|
||||
cloud: 'AzureCloud',
|
||||
userIdentityFallbackCredentialsEnabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user