diff --git a/conf/defaults.ini b/conf/defaults.ini index 7221a9e7e79..af1be193f6a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -812,6 +812,7 @@ client_id = some_client_id client_secret = managed_identity_client_id = federated_credential_audience = +workload_identity_token_file = /var/run/secrets/azure/tokens/azure-identity-token scopes = openid email profile auth_url = https://login.microsoftonline.com//oauth2/v2.0/authorize token_url = https://login.microsoftonline.com//oauth2/v2.0/token @@ -1841,7 +1842,7 @@ forward_host_env_vars = preinstall = # Comma separated list of plugin ids to install before the startup process # These will be installed before starting Grafana. Useful when used with provisioning. -preinstall_sync = +preinstall_sync = # Disables preinstall feature. It has the same effect as setting preinstall to an empty list. preinstall_disabled = false # Update strategy for plugins. diff --git a/conf/sample.ini b/conf/sample.ini index 4200522661e..752088e409f 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -787,6 +787,7 @@ ;client_secret = some_client_secret ;managed_identity_client_id = ;federated_credential_audience = +;workload_identity_token_file = ;scopes = openid email profile ;auth_url = https://login.microsoftonline.com//oauth2/v2.0/authorize ;token_url = https://login.microsoftonline.com//oauth2/v2.0/token @@ -1787,7 +1788,7 @@ default_datasource_uid = ; preinstall = # Comma separated list of plugin ids to install before the startup process # These will be installed before starting Grafana. Useful when used with provisioning. -; preinstall_sync = +; preinstall_sync = # Disables preinstall feature. It has the same effect as setting preinstall to an empty list. ; preinstall_disabled = false diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index bbaa685af3e..0b6cbec6a95 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -65,19 +65,53 @@ To enable the Azure AD/Entra ID OAuth, register your application with Entra ID. - **Federated credentials** - 1. Refer to [Configure an application to trust a managed identity (preview)](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center) for a complete guide on setting up a managed identity as a federated credential. - Add a new entry under Federated credentials with the following configuration. + - **_Managed Identity_** - - Federated credential scenario: Select **Other issuer**. - - Issuer: The OAuth 2.0 / OIDC issuer URL of the Microsoft Entra ID authority. For example: `https://login.microsoftonline.com/{tenantID}/v2.0`. - - Subject identifier: The Object (Principal) ID GUID of the Managed Identity. - - Name: A unique descriptive name for the credential. - - Description: Grafana OAuth. - - Audience: The audience value that must appear in the external token. For Public cloud, it would be `api://AzureADTokenExchange`. See mentioned documentation for the full list of available audiences. + 1. Refer to [Configure an application to trust a managed identity (preview)](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center) for a complete guide on setting up a managed identity as a federated credential. + Add a new entry under Federated credentials with the following configuration. - 1. Click **Add**, and then copy the Managed Identity Client ID and the federated credential Audience values. This is your OAuth 2.0 federated credential. + - Federated credential scenario: Select **Other issuer**. + - Issuer: The OAuth 2.0 / OIDC issuer URL of the Microsoft Entra ID authority. For example: `https://login.microsoftonline.com/{tenantID}/v2.0`. + - Subject identifier: The Object (Principal) ID GUID of the Managed Identity. + - Name: A unique descriptive name for the credential. + - Description: Grafana OAuth. + - Audience: The audience value that must appear in the external token. For Public cloud, it would be `api://AzureADTokenExchange`. See mentioned documentation for the full list of available audiences. - 1. You must have set `client_authentication` under `[auth.azuread]` to `managed_identity` in the Grafana server configuration for this to work. + 1. Click **Add**, and then copy the Managed Identity Client ID and the federated credential Audience values. This is your OAuth 2.0 federated credential. + + 1. You must have set `client_authentication` under `[auth.azuread]` to `managed_identity` in the Grafana server configuration for this to work. + + {{< admonition type="note" >}} + Managed identities as federated credentials are only applicable to workloads hosted in Azure. + + You can only add user-assigned managed identities as federated credentials on Entra ID applications. + {{< /admonition >}} + + - **_Workload Identity (K8s/AKS)_** + + 1. Refer to [Federated identity credential for an Azure AD application](https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html#azure-portal-ui) for a complete guide on setting up a federated credential for workload identity. + Add a new entry under Federated credentials with the following configuration. + + - Federated credential scenario: Select **Kubernetes accessing Azure resources**. + - [Cluster issuer URL](https://learn.microsoft.com/en-us/azure/aks/use-oidc-issuer#get-the-oidc-issuer-url): The OIDC issuer URL that your cluster is integrated with. For example: `https://{region}.oic.prod-aks.azure.com/{tenant_id}/{uuid}`. + - Namespace: Namespace of your Grafana deployment. For example: `grafana`. + - Service account name: Service account name of your Grafana deployment. For example: `grafana`. + - Subject identifier: The expected identity (subject claim) from the OIDC token, which Azure uses to validate and authorize token issuance to the requesting workload. For example: `system:serviceaccount:grafana:grafana`. + - Name: A unique descriptive name for the credential. + - Description: Grafana OAuth. + - Audience: The audience value that must appear in the external token. For Public cloud, it would be `api://AzureADTokenExchange`. See mentioned documentation for the full list of available audiences. + + 1. You must have set `client_authentication` (env var `GF_AUTH_AZUREAD_CLIENT_AUTHENTICATION`) under `[auth.azuread]` to `workload_identity` in the Grafana server configuration for this to work. + + 1. You may optionally set `workload_identity_token_file` (env var `GF_AUTH_AZUREAD_WORKLOAD_IDENTITY_TOKEN_FILE`) under `[auth.azuread]` to `/var/run/secrets/azure/tokens/azure-identity-token` in the Grafana server configuration for this to work. (Optional, defaults to `/var/run/secrets/azure/tokens/azure-identity-token`) + + 1. You must have set `client_id` (env var `GF_AUTH_AZUREAD_CLIENT_ID`) under `[auth.azuread]` in the Grafana server configuration for this to work. This must match the Entra ID/Azure AD App Registration Application (client) ID. + + 1. You must have set `token_url` (env var `GF_AUTH_AZUREAD_TOKEN_URL`) under `[auth.azuread]` to `https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/token` in the Grafana server configuration for this to work. + + 1. You must have set `auth_url` (env var `GF_AUTH_AZUREAD_AUTH_URL`) under `[auth.azuread]` to `https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/authorize` in the Grafana server configuration for this to work. + + 1. You must have set `federated_credential_audience` (env var `GF_AUTH_AZUREAD_FEDERATED_CREDENTIAL_AUDIENCE`) under `[auth.azuread]` to `api://AzureADTokenExchange` in the Grafana server configuration for this to work. {{< admonition type="note" >}} Managed identities as federated credentials are only applicable to workloads hosted in Azure. @@ -498,32 +532,35 @@ skip_org_role_sync = true The following table outlines the various Azure AD/Entra ID configuration options. You can apply these options as environment variables, similar to any other configuration within Grafana. For more information, refer to [Override configuration with environment variables](../../../configure-grafana/#override-configuration-with-environment-variables). -| Setting | Required | Supported on Cloud | Description | Default | -| ---------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `enabled` | No | Yes | Enables Azure AD/Entra ID authentication. | `false` | -| `name` | No | Yes | Name that refers to the Azure AD/Entra ID authentication from the Grafana user interface. | `OAuth` | -| `icon` | No | Yes | Icon used for the Azure AD/Entra ID authentication in the Grafana user interface. | `signin` | -| `client_id` | Yes | Yes | Client ID of the App (`Application (client) ID` on the **App registration** dashboard). | | -| `client_secret` | Yes | Yes | Client secret of the App. | | -| `auth_url` | Yes | Yes | Authorization endpoint of the Azure AD/Entra ID OAuth2 provider. | | -| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | | -| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | -| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `openid email profile` | -| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Azure AD/Entra ID login. Only existing Grafana users can log in with Azure AD/Entra ID if set to `false`. | `true` | -| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | -| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Map roles](#map-roles). | `false` | -| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | -| `org_mapping` | No | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | -| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Azure AD/Entra ID user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles](#map-roles). | `false` | -| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | -| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Azure AD/Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal](#configure-group-membership-claims-on-the-azure-portal). | | -| `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | -| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | -| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | -| `tls_client_cert` | No | No | The path to the certificate. | | -| `tls_client_key` | No | No | The path to the key. | | -| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | -| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | -| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` | -| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` | -| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | +| Setting | Required | Supported on Cloud | Description | Default | +| ------------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| `enabled` | No | Yes | Enables Azure AD/Entra ID authentication. | `false` | +| `name` | No | Yes | Name that refers to the Azure AD/Entra ID authentication from the Grafana user interface. | `OAuth` | +| `icon` | No | Yes | Icon used for the Azure AD/Entra ID authentication in the Grafana user interface. | `signin` | +| `client_authentication` | Yes | Yes | Defines the client authentication method used to authenticate to the token endpoint. Supported values: `none`, `client_secret_post`, `managed_identity`, or `workload_identity`. | | +| `workload_identity_token_file` | No | Yes | The path to the token file used to authenticate to the OAuth2 provider. This is only required when `client_authentication` is set to `workload_identity`. The token file contains the service account token projected by Kubernetes. | `/var/run/secrets/azure/tokens/azure-identity-token` | +| `federated_credential_audience` | No | Yes | The audience of the federated identity credential of your OAuth2 app. Required when `client_authentication` is set to `managed_identity` or `workload_identity`. For public cloud, this is typically `api://AzureADTokenExchange`. | `api://AzureADTokenExchange` | +| `client_id` | Yes | Yes | Client ID of the App (`Application (client) ID` on the **App registration** dashboard). | | +| `client_secret` | Yes | Yes | Client secret of the App. | | +| `auth_url` | Yes | Yes | Authorization endpoint of the Azure AD/Entra ID OAuth2 provider. | | +| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | | +| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | +| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `openid email profile` | +| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Azure AD/Entra ID login. Only existing Grafana users can log in with Azure AD/Entra ID if set to `false`. | `true` | +| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | +| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Map roles](#map-roles). | `false` | +| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `org_mapping` | No | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Azure AD/Entra ID user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles](#map-roles). | `false` | +| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | +| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Azure AD/Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal](#configure-group-membership-claims-on-the-azure-portal). | | +| `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | +| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | +| `tls_client_cert` | No | No | The path to the certificate. | | +| `tls_client_key` | No | No | The path to the key. | | +| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | +| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | +| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` | +| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` | +| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index 6d351ce4b74..d09265407f3 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -385,6 +385,12 @@ func validateClientAuthentication(info *social.OAuthInfo, requester identity.Req } return nil + case social.WorkloadIdentity: + if info.WorkloadIdentityTokenFile == "" { + return ssosettings.ErrInvalidOAuthConfig("Workload identity token file is required for Workload identity authentication.") + } + return nil + case social.ClientSecretPost, "": if info.ClientSecret == "" { return ssosettings.ErrInvalidOAuthConfig("Client secret is required for Client secret authentication.") diff --git a/pkg/login/social/connectors/social_base.go b/pkg/login/social/connectors/social_base.go index 558d5105fab..35bc67004c5 100644 --- a/pkg/login/social/connectors/social_base.go +++ b/pkg/login/social/connectors/social_base.go @@ -124,6 +124,7 @@ func (s *SocialBase) getBaseSupportBundleContent(bf *bytes.Buffer) error { fmt.Fprintf(bf, "client_secret = %v ; issue if empty\n", strings.Repeat("*", len(s.ClientSecret))) fmt.Fprintf(bf, "managed_identity_client_id = %v\n", s.info.ManagedIdentityClientID) fmt.Fprintf(bf, "federated_credential_audience = %v\n", s.info.FederatedCredentialAudience) + fmt.Fprintf(bf, "workload_identity_token_file = %v\n", s.info.WorkloadIdentityTokenFile) fmt.Fprintf(bf, "auth_url = %v\n", s.Endpoint.AuthURL) fmt.Fprintf(bf, "token_url = %v\n", s.Endpoint.TokenURL) fmt.Fprintf(bf, "auth_style = %v\n", s.Endpoint.AuthStyle) diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 741d02098a9..daad4db4ee7 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -18,7 +18,8 @@ const ( ClientSecretPost = "client_secret_post" None = "none" // Azure AD - ManagedIdentity = "managed_identity" + ManagedIdentity = "managed_identity" + WorkloadIdentity = "workload_identity" // Other providers... AzureADProviderName = "azuread" @@ -72,6 +73,7 @@ type OAuthInfo struct { ClientId string `mapstructure:"client_id" toml:"client_id"` ClientSecret string `mapstructure:"client_secret" toml:"-"` ManagedIdentityClientID string `mapstructure:"managed_identity_client_id" toml:"managed_identity_client_id"` + WorkloadIdentityTokenFile string `mapstructure:"workload_identity_token_file" toml:"workload_identity_token_file"` FederatedCredentialAudience string `mapstructure:"federated_credential_audience" toml:"federated_credential_audience"` EmailAttributeName string `mapstructure:"email_attribute_name" toml:"email_attribute_name"` EmailAttributePath string `mapstructure:"email_attribute_path" toml:"email_attribute_path"` diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index ed469ba9f17..1c6429c3cc4 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net/url" + "os" "strings" "golang.org/x/oauth2" @@ -137,7 +138,21 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden } clientCtx := context.WithValue(ctx, oauth2.HTTPClient, httpClient) + // exchange auth code to a valid token + if oauthCfg.ClientAuthentication == social.WorkloadIdentity { + federatedToken, err := os.ReadFile(oauthCfg.WorkloadIdentityTokenFile) + if err != nil { + return nil, fmt.Errorf("failed to read workload identity token file: %w", err) + } + + opts = append(opts, + oauth2.SetAuthURLParam("client_id", oauthCfg.ClientId), + oauth2.SetAuthURLParam("client_assertion", strings.TrimSpace(string(federatedToken))), + oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), + ) + } + token, err := connector.Exchange(clientCtx, r.HTTPRequest.URL.Query().Get("code"), opts...) if err != nil { return nil, errOAuthTokenExchange.Errorf("failed to exchange code to token: %w", err) diff --git a/pkg/services/ssosettings/strategies/oauth_strategy.go b/pkg/services/ssosettings/strategies/oauth_strategy.go index aa21dad6725..a9689008235 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy.go @@ -75,6 +75,7 @@ func (s *OAuthStrategy) loadSettingsForProvider(provider string) map[string]any "client_secret": section.Key("client_secret").Value(), "managed_identity_client_id": section.Key("managed_identity_client_id").Value(), "federated_credential_audience": section.Key("federated_credential_audience").Value(), + "workload_identity_token_file": section.Key("workload_identity_token_file").Value(), "scopes": section.Key("scopes").Value(), "empty_scopes": section.Key("empty_scopes").MustBool(false), "auth_style": section.Key("auth_style").Value(), diff --git a/pkg/services/ssosettings/strategies/oauth_strategy_test.go b/pkg/services/ssosettings/strategies/oauth_strategy_test.go index bc836879f9d..c76aba9c839 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy_test.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy_test.go @@ -24,6 +24,7 @@ var ( client_secret = test_client_secret managed_identity_client_id = test_managed_identity_client_id federated_credential_audience = test_federated_credential_audience + workload_identity_token_file = test_workload_identity_token_file scopes = openid, profile, email empty_scopes = false email_attribute_name = email:primary @@ -70,6 +71,7 @@ var ( "client_secret": "test_client_secret", "managed_identity_client_id": "test_managed_identity_client_id", "federated_credential_audience": "test_federated_credential_audience", + "workload_identity_token_file": "test_workload_identity_token_file", "scopes": "openid, profile, email", "empty_scopes": false, "email_attribute_name": "email:primary", @@ -125,6 +127,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { [auth.azuread] force_use_graph_api = true allowed_organizations = org1, org2 + workload_identity_token_file = azuread_token_file [auth.github] team_ids = first, second @@ -159,6 +162,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { require.Equal(t, true, result["force_use_graph_api"]) require.Equal(t, "org1, org2", result["allowed_organizations"]) + require.Equal(t, "azuread_token_file", result["workload_identity_token_file"]) }) t.Run(social.GitHubProviderName, func(t *testing.T) { @@ -210,7 +214,7 @@ func TestGetProviderConfig_GrafanaComGrafanaNet(t *testing.T) { [auth.grafana_com] enabled = true client_id = grafanaComClientId - + [auth.grafananet] enabled = false client_id = grafanaNetClientId`, @@ -225,7 +229,7 @@ func TestGetProviderConfig_GrafanaComGrafanaNet(t *testing.T) { [auth.grafana_com] enabled = false client_id = grafanaComClientId - + [auth.grafananet] enabled = true client_id = grafanaNetClientId`, @@ -240,7 +244,7 @@ func TestGetProviderConfig_GrafanaComGrafanaNet(t *testing.T) { [auth.grafana_com] enabled = true client_id = grafanaComClientId - + [auth.grafananet] enabled = true client_id = grafanaNetClientId`, @@ -255,7 +259,7 @@ func TestGetProviderConfig_GrafanaComGrafanaNet(t *testing.T) { [auth.grafana_com] enabled = false client_id = grafanaComClientId - + [auth.grafananet] enabled = false client_id = grafanaNetClientId`, diff --git a/public/app/features/auth-config/FieldRenderer.tsx b/public/app/features/auth-config/FieldRenderer.tsx index feb4f9aeb40..1d80f835fb0 100644 --- a/public/app/features/auth-config/FieldRenderer.tsx +++ b/public/app/features/auth-config/FieldRenderer.tsx @@ -7,7 +7,7 @@ import { Checkbox, Field, Input, SecretInput, Select, Switch, useTheme2 } from ' import { fieldMap } from './fields'; import { SSOProviderDTO, SSOSettingsField } from './types'; -import { isSelectableValue } from './utils/guards'; +import { isSelectableValueArray } from './utils/guards'; interface FieldRendererProps extends Pick< @@ -123,7 +123,7 @@ export const FieldRenderer = ({ const watchOptions = watch(name); let options = fieldData.options; if (!fieldData.options?.length) { - options = isSelectableValue(watchOptions) ? watchOptions : []; + options = isSelectableValueArray(watchOptions) ? watchOptions : []; } return ( diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index a93b5e7ebb7..d787f4c1be8 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -9,7 +9,7 @@ import { contextSrv } from 'app/core/core'; import { ServerDiscoveryField } from './components/ServerDiscoveryField'; import { FieldData, SSOProvider, SSOSettingsField } from './types'; -import { isSelectableValue } from './utils/guards'; +import { isSelectableValue, isSelectableValueArray } from './utils/guards'; import { isUrlValid } from './utils/url'; type Section = Record< @@ -53,7 +53,7 @@ export const getSectionFields = (): Section => { fields: ['roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'], }, { - name: 'Extra security measures', + name: extraSecurityLabel, id: 'extra', fields: [ 'allowedOrganizations', @@ -66,6 +66,7 @@ export const getSectionFields = (): Section => { 'tlsClientCert', 'tlsClientKey', 'tlsClientCa', + 'workloadIdentityTokenFile', ], }, ], @@ -284,6 +285,7 @@ const authURLLabel = 'Auth URL'; const tokenURLLabel = 'Token URL'; const apiURLLabel = 'API URL'; const jmesPathLabel = 'JMESPath'; +const workloadIdentityLabel = 'Workload identity'; /** * List all the fields that can be used in the form @@ -357,6 +359,32 @@ export function fieldMap(provider: string): Record { 'The audience of the federated identity credential of your OAuth2 app.' ), }, + workloadIdentityTokenFile: { + label: t('auth-config.fields.workload-identity-token-file-label', '{{ workloadIdentityLabel }} token file', { + workloadIdentityLabel, + }), + type: 'text', + description: t( + 'auth-config.fields.workload-identity-token-file-description', + 'The file path to the token file used to authenticate to the OAuth2 provider. This is only required when client authentication is set to "workload_identity". Defaults to /var/run/secrets/azure/tokens/azure-identity-token.' + ), + validation: { + validate: (value, formValues) => { + let clientAuth = formValues.clientAuthentication; + if (isSelectableValue(clientAuth)) { + clientAuth = clientAuth.value; + } + if (clientAuth === 'workload_identity') { + return !!value; + } + return true; + }, + message: t( + 'auth-config.fields.workload-identity-token-file-required', + 'This field must be set when client authentication is set to "Workload identity".' + ), + }, + }, allowedOrganizations: { label: t('auth-config.fields.allowed-organizations-label', 'Allowed organizations'), type: 'select', @@ -465,7 +493,7 @@ export function fieldMap(provider: string): Record { if (typeof value === 'string') { return uuidValidate(value); } - if (isSelectableValue(value)) { + if (isSelectableValueArray(value)) { return value.every((v) => v?.value && uuidValidate(v.value)); } return true; @@ -809,7 +837,7 @@ export function fieldMap(provider: string): Record { if (typeof value === 'string') { return isNumeric(value); } - if (isSelectableValue(value)) { + if (isSelectableValueArray(value)) { return value.every((v) => v?.value && isNumeric(v.value)); } return true; @@ -893,6 +921,7 @@ function clientAuthenticationOptions(provider: string): Array typeof v === 'object' && v !== null && 'value' in v); } + +export function isSelectableValue(value: unknown): value is SelectableValue { + return typeof value === 'object' && value !== null && 'value' in value; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2b38b32f5fc..f8f9c04ca79 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3094,7 +3094,10 @@ "use-refresh-token-description": "If enabled, Grafana will fetch a new access token using the refresh token provided by the OAuth2 provider.", "use-refresh-token-label": "Use refresh token", "validate-hosted-domain-description": "If enabled, Grafana will match the Hosted Domain retrieved from the Google ID Token against the \"{{ allowedDomainsLabel }}\" list specified by the user.", - "validate-hosted-domain-label": "Validate hosted domain" + "validate-hosted-domain-label": "Validate hosted domain", + "workload-identity-token-file-description": "The file path to the token file used to authenticate to the OAuth2 provider. This is only required when client authentication is set to \"workload_identity\". Defaults to /var/run/secrets/azure/tokens/azure-identity-token.", + "workload-identity-token-file-label": "{{ workloadIdentityLabel }} token file", + "workload-identity-token-file-required": "This field must be set when client authentication is set to \"Workload identity\"." }, "get-page-nav": { "subTitle": {