MSSQL: Current-user authentication (#113977)
* Moving things around
* Update frontend to support CUA
* Add CUA support to backend
* Copy parseURL function to where it's used
* Update test
* Remove experimental-strip-types
* Docs
* A bit more of a refactor to reduce complexity
* Revert "Remove experimental-strip-types"
This reverts commit 70fbc1c0cd.
* Review
* Docs updates
* Another docs fix
This commit is contained in:
@@ -74,6 +74,21 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
|
||||
configure-grafana-azure-auth:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/
|
||||
configure-grafana-azure:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#azure
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#azure
|
||||
configure-grafana-azure-auth-scopes:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
|
||||
---
|
||||
|
||||
# Configure the Microsoft SQL Server data source
|
||||
@@ -138,14 +153,19 @@ If you're using an older version of Microsoft SQL Server like 2008 and 2008R2, y
|
||||
|
||||
**Authentication:**
|
||||
|
||||
| Authentication Type | Description | Credentials / Fields |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username<br>- **Password**: SQL Server password |
|
||||
| **Windows Authentication**<br>(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials |
|
||||
| **Windows AD**<br>(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`<br>- **Password**: Active Directory password |
|
||||
| **Windows AD**<br>(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`<br>- **Keytab file path**: Path to your keytab file |
|
||||
| **Windows AD**<br>(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) |
|
||||
| **Windows AD**<br>(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`<br>- **Credential cache file path**: e.g., `/home/grot/cache.json` |
|
||||
{{< admonition type="note" >}}
|
||||
In order to use Azure AD Authentication the toggle `auth.azure_auth_enabled` must be set to `true` in the Grafana configuration file.
|
||||
{{< /admonition >}}
|
||||
|
||||
| Authentication Type | Description | Credentials / Fields |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username<br>- **Password**: SQL Server password |
|
||||
| **Windows Authentication**<br>(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials |
|
||||
| **Windows AD**<br>(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`<br>- **Password**: Active Directory password |
|
||||
| **Windows AD**<br>(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`<br>- **Keytab file path**: Path to your keytab file |
|
||||
| **Windows AD**<br>(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) |
|
||||
| **Windows AD**<br>(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`<br>- **Credential cache file path**: e.g., `/home/grot/cache.json` |
|
||||
| **Azure Entra ID (formerly Azure AD) Authentication** | Authenticates the data source using Azure authentication methods. | Details on the supported authentication methods and how to configure them can be found in the [Azure authentication section](./index.md#azure-entra-id-formerly-azure-ad-authentication). |
|
||||
|
||||
**Additional settings:**
|
||||
|
||||
@@ -185,6 +205,123 @@ After configuring your MSSQL data source options, click **Save & test** at the b
|
||||
|
||||
**Database Connection OK**
|
||||
|
||||
### Azure Entra ID (formerly Azure AD) Authentication
|
||||
|
||||
The following Azure authentication methods are supported:
|
||||
|
||||
- Current User authentication
|
||||
- App Registration
|
||||
- Managed Identity
|
||||
- Azure Entra Password
|
||||
|
||||
The Azure SQL Server that you are connecting to should support Azure Entra authentication to support adding the App Registration as a user in the database. For configuration details, refer to the [Azure SQL documentation](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure?view=azuresql&tabs=azure-portal).
|
||||
|
||||
#### Current User authentication
|
||||
|
||||
This is the recommended authentication mechanism when working with SQL Server instances that are hosted in Azure. It allows users to be authenticated to and query the database using their own credentials rather than long-lived credentials.
|
||||
|
||||
This authentication method requires your Grafana instance to be configured with Azure Entra ID (formerly Active Directory) authentication for login. With Azure Entra ID login, this 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](ref: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 **APIs my organization uses** > Search for **Azure SQL** and select it > **Delegated permissions** > `user_impersonation` > **Add permissions**
|
||||
|
||||
After 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. After 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. Also, note that query and resource caching is disabled by default for data sources using current user authentication.
|
||||
|
||||
**To enable current user authentication for Grafana:**
|
||||
|
||||
1. Set the `user_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
user_identity_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Current User**.
|
||||
|
||||
### App Registration
|
||||
|
||||
You must create an app registration and service principal in Azure Entra to authenticate the data source.
|
||||
For configuration details, refer to the [Azure documentation for service principals](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in).
|
||||
|
||||
After the app registration has been created, make note of the tenant ID, client ID, and client secret. Take the following steps to add the app registration as a SQL user:
|
||||
|
||||
1. Connect to your Azure SQL database as a user with administrative permissions (the user used here must have the ability to read your Azure Entra directory e.g. by possessing the `Directory Readers` role).
|
||||
2. Run `CREATE USER [$IDENTITY_NAME] FROM EXTERNAL PROVIDER;`, substituting `IDENTITY_NAME` with the app registration name.
|
||||
3. Grant the created user the appropriate level of permissions for your use-case. It is recommended that users configured for data sources only have reader permissions.
|
||||
|
||||
After the appropriate permissions have been granted, configure the SQL Server data source to use the app registration:
|
||||
|
||||
1. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **App Registration**.
|
||||
2. Set the **Azure Cloud** value to the correct value. If you are using the Azure public cloud this will be **Azure**.
|
||||
3. Set the **Directory (tenant) ID**, **Application (client) ID**, and **Client Secret** values to those for your app registration.
|
||||
|
||||
### Managed Identity
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Managed Identity is available only in [Azure Managed Grafana](https://azure.microsoft.com/en-us/products/managed-grafana) or Grafana OSS/Enterprise when deployed in Azure. It is not available in Grafana Cloud.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can use managed identity to configure SQL Server 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.
|
||||
This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations.
|
||||
For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview).
|
||||
|
||||
**To enable managed identity for Grafana:**
|
||||
|
||||
1. Set the `managed_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
managed_identity_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Managed Identity**.
|
||||
|
||||
This hides the directory ID, application ID, and client secret fields, and the data source uses managed identity to authenticate to SQL Server.
|
||||
|
||||
3. You can set the `managed_identity_client_id` field in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure) to allow a user-assigned managed identity to be used instead of the default system-assigned identity.
|
||||
|
||||
Ensure that the managed identity used is added to your Azure SQL instance as a user.
|
||||
|
||||
### Azure Entra Password
|
||||
|
||||
{{< admonition type="warning" >}}
|
||||
Azure Entra Password is not a recommended authentication mechanism as it requires configuration using a single users password. Consider an alternative authentication method such as current user authentication or app registration.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can connect to an Azure SQL database using the username and password of a user that has permissions in the desired database. This also requires an app registration to be configured with access to the database.
|
||||
|
||||
**To enable Azure Entra password for Grafana:**
|
||||
|
||||
1. Set the `azure_entra_password_credentials_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
azure_entra_password_credentials_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Azure Entra Password**.
|
||||
3. Set the **User ID** value to the username of the user in the Azure SQL database.
|
||||
4. Set the **Application Client ID** to the client ID of the app registration that has been added to the Azure SQL database
|
||||
5. Set the **Password** value to the password of the user in the Azure SQL database.
|
||||
|
||||
### Min time interval
|
||||
|
||||
The **Min time interval** setting defines a lower limit for the [`$__interval`](ref:add-template-variables-interval) and [`$__interval_ms`][add-template-variables-interval_ms] variables.
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azcredentials"
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
|
||||
)
|
||||
|
||||
func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool) (string, error) {
|
||||
func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureSettings *azsettings.AzureSettings, userAssertion string) (string, error) {
|
||||
connStr := ""
|
||||
switch c := azureCredentials.(type) {
|
||||
case *azcredentials.AzureManagedIdentityCredentials:
|
||||
if azureManagedIdentityClientId != "" {
|
||||
connStr += fmt.Sprintf("user id=%s;", azureManagedIdentityClientId)
|
||||
if azureSettings.ManagedIdentityClientId != "" {
|
||||
connStr += fmt.Sprintf("user id=%s;", azureSettings.ManagedIdentityClientId)
|
||||
}
|
||||
connStr += fmt.Sprintf("fedauth=%s;",
|
||||
"ActiveDirectoryManagedIdentity")
|
||||
@@ -23,7 +24,7 @@ func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia
|
||||
"ActiveDirectoryApplication",
|
||||
)
|
||||
case *azcredentials.AzureEntraPasswordCredentials:
|
||||
if azureEntraPasswordCredentialsEnabled {
|
||||
if azureSettings.AzureEntraPasswordCredentialsEnabled {
|
||||
connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;",
|
||||
c.UserId,
|
||||
c.Password,
|
||||
@@ -33,6 +34,12 @@ func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia
|
||||
} else {
|
||||
return "", fmt.Errorf("azure entra password authentication is not enabled")
|
||||
}
|
||||
case *azcredentials.AadCurrentUserCredentials:
|
||||
if userAssertion == "" {
|
||||
return "", fmt.Errorf("user ID token is empty but required for current user authentication")
|
||||
}
|
||||
connStr += fmt.Sprintf("user id=%s;userassertion=%s;password=%s;fedauth=%s;",
|
||||
azureSettings.UserIdentityTokenEndpoint.ClientId, userAssertion, azureSettings.UserIdentityTokenEndpoint.ClientSecret, "ActiveDirectoryOnBehalfOf")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported azure authentication type")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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/instancemgmt"
|
||||
@@ -43,7 +44,8 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dsHandler.QueryData(ctx, req)
|
||||
|
||||
return dsHandler.QueryData(azusercontext.WithUserFromQueryReq(ctx, req), req)
|
||||
}
|
||||
|
||||
func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.InstanceFactoryFunc {
|
||||
@@ -53,6 +55,8 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginCfg := backend.PluginConfigFromContext(ctx)
|
||||
|
||||
jsonData := sqleng.JsonData{
|
||||
MaxOpenConns: sqlCfg.DefaultMaxOpenConns,
|
||||
MaxIdleConns: sqlCfg.DefaultMaxIdleConns,
|
||||
@@ -87,6 +91,7 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc
|
||||
Updated: settings.Updated,
|
||||
UID: settings.UID,
|
||||
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
||||
OrgID: pluginCfg.OrgID,
|
||||
}
|
||||
|
||||
userFacingDefaultError, err := grafCfg.UserFacingDefaultError()
|
||||
@@ -117,5 +122,5 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dsHandler.CheckHealth(ctx, req)
|
||||
return dsHandler.CheckHealth(azusercontext.WithUserFromHealthCheckReq(ctx, req), req)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package sqleng
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azcredentials"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||
"github.com/grafana/grafana/pkg/tsdb/mssql/azure"
|
||||
"github.com/grafana/grafana/pkg/tsdb/mssql/kerberos"
|
||||
"github.com/grafana/grafana/pkg/tsdb/mssql/utils"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/microsoft/go-mssqldb/azuread"
|
||||
)
|
||||
|
||||
func newMSSQL(ctx context.Context, driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, error) {
|
||||
func newMSSQL(driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, proxyClient proxy.Client) (*sql.DB, error) {
|
||||
var connector *mssql.Connector
|
||||
var err error
|
||||
if driverName == "azuresql" {
|
||||
@@ -31,12 +31,6 @@ func newMSSQL(ctx context.Context, driverName string, rowLimit int64, dsInfo Dat
|
||||
return nil, fmt.Errorf("mssql connector creation failed")
|
||||
}
|
||||
|
||||
proxyClient, err := settings.ProxyClient(ctx)
|
||||
if err != nil {
|
||||
logger.Error("mssql proxy creation failed", "error", err)
|
||||
return nil, fmt.Errorf("mssql proxy creation failed")
|
||||
}
|
||||
|
||||
if proxyClient.SecureSocksProxyEnabled() {
|
||||
dialer, err := proxyClient.NewSecureSocksProxyContextDialer()
|
||||
if err != nil {
|
||||
@@ -81,7 +75,7 @@ const (
|
||||
kerberosCredentialCacheFile = "Windows AD: Credential cache file" // #nosec G101
|
||||
)
|
||||
|
||||
func generateConnectionString(dsInfo DataSourceInfo, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) {
|
||||
func generateConnectionString(dsInfo DataSourceInfo, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger, azureSettings *azsettings.AzureSettings, userAssertion string) (string, error) {
|
||||
const dfltPort = "0"
|
||||
var addr util.NetworkAddress
|
||||
if dsInfo.URL != "" {
|
||||
@@ -119,7 +113,7 @@ func generateConnectionString(dsInfo DataSourceInfo, azureManagedIdentityClientI
|
||||
|
||||
switch dsInfo.JsonData.AuthenticationType {
|
||||
case azureAuthentication:
|
||||
azureCredentialDSNFragment, err := azure.GetAzureCredentialDSNFragment(azureCredentials, azureManagedIdentityClientId, azureEntraPasswordCredentialsEnabled)
|
||||
azureCredentialDSNFragment, err := azure.GetAzureCredentialDSNFragment(azureCredentials, azureSettings, userAssertion)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -13,7 +13,15 @@ import (
|
||||
)
|
||||
|
||||
func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
if err := e.db.Ping(); err != nil {
|
||||
db, err := e.getDB(ctx)
|
||||
if err != nil {
|
||||
logCheckHealthError(ctx, e.dsInfo, err)
|
||||
if strings.EqualFold(req.PluginContext.User.Role, "Admin") {
|
||||
return ErrToHealthCheckResult(err)
|
||||
}
|
||||
return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: e.TransformQueryError(e.log, err).Error()}, nil
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
logCheckHealthError(ctx, e.dsInfo, err)
|
||||
if strings.EqualFold(req.PluginContext.User.Role, "Admin") {
|
||||
return ErrToHealthCheckResult(err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package sqleng
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -16,9 +17,11 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azcredentials"
|
||||
"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/gtime"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
@@ -74,6 +77,7 @@ type DataSourceInfo struct {
|
||||
Updated time.Time
|
||||
UID string
|
||||
DecryptedSecureJSONData map[string]string
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
type DataPluginConfiguration struct {
|
||||
@@ -97,6 +101,8 @@ type DataSourceHandler struct {
|
||||
azureCredentials azcredentials.AzureCredentials
|
||||
kerberosAuth kerberos.KerberosAuth
|
||||
driverName string
|
||||
proxyClient proxy.Client
|
||||
dbConnections sync.Map
|
||||
}
|
||||
|
||||
type QueryJson struct {
|
||||
@@ -142,6 +148,12 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc
|
||||
return nil, fmt.Errorf("error getting kerberos settings: %w", err)
|
||||
}
|
||||
|
||||
proxyClient, err := settings.ProxyClient(ctx)
|
||||
if err != nil {
|
||||
logger.Error("mssql proxy creation failed", "error", err)
|
||||
return nil, fmt.Errorf("mssql proxy creation failed")
|
||||
}
|
||||
|
||||
queryDataHandler := DataSourceHandler{
|
||||
queryResultTransformer: &queryResultTransformer,
|
||||
macroEngine: newMssqlMacroEngine(),
|
||||
@@ -154,6 +166,7 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc
|
||||
azureCredentials: azureCredentials,
|
||||
kerberosAuth: kerberosAuth,
|
||||
driverName: driverName,
|
||||
proxyClient: proxyClient,
|
||||
}
|
||||
|
||||
if len(config.TimeColumnNames) > 0 {
|
||||
@@ -164,18 +177,21 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc
|
||||
queryDataHandler.metricColumnTypes = config.MetricColumnTypes
|
||||
}
|
||||
|
||||
cnnstr, err := generateConnectionString(config.DSInfo, azureSettings.ManagedIdentityClientId, azureSettings.AzureEntraPasswordCredentialsEnabled, azureCredentials, kerberosAuth, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Every auth method besides Azure AD Current User Identity can use a persistent DB connection
|
||||
if config.DSInfo.JsonData.AuthenticationType != azureAuthentication || azureCredentials.AzureAuthType() != azcredentials.AzureAuthCurrentUserIdentity {
|
||||
cnnstr, err := generateConnectionString(config.DSInfo, azureCredentials, kerberosAuth, log, azureSettings, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := newMSSQL(ctx, driverName, config.RowLimit, config.DSInfo, cnnstr, log, settings)
|
||||
if err != nil {
|
||||
logger.Error("Failed connecting to MSSQL", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
db, err := newMSSQL(driverName, config.RowLimit, config.DSInfo, cnnstr, log, proxyClient)
|
||||
if err != nil {
|
||||
logger.Error("Failed connecting to MSSQL", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queryDataHandler.db = db
|
||||
queryDataHandler.db = db
|
||||
}
|
||||
|
||||
return &queryDataHandler, nil
|
||||
}
|
||||
@@ -192,9 +208,52 @@ func (e *DataSourceHandler) Dispose() {
|
||||
e.log.Error("Failed to dispose db", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any cached user-specific connections
|
||||
e.dbConnections.Range(func(_, conn interface{}) bool {
|
||||
_ = conn.(*sql.DB).Close()
|
||||
return true
|
||||
})
|
||||
e.dbConnections.Clear()
|
||||
|
||||
e.log.Debug("DB disposed")
|
||||
}
|
||||
|
||||
func (e *DataSourceHandler) getDB(ctx context.Context) (*sql.DB, error) {
|
||||
e.log.Debug("Getting DB...")
|
||||
if e.dsInfo.JsonData.AuthenticationType != azureAuthentication || e.azureCredentials.AzureAuthType() != azcredentials.AzureAuthCurrentUserIdentity {
|
||||
if e.db == nil {
|
||||
return nil, fmt.Errorf("database connection is not initialized")
|
||||
}
|
||||
return e.db, nil
|
||||
}
|
||||
|
||||
userCtx, ok := azusercontext.GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get user from context for Azure Current User authentication")
|
||||
}
|
||||
cacheKey := fmt.Sprintf("mssql-%d-%x-%x-%s", e.dsInfo.OrgID, sha256.Sum256([]byte(userCtx.User.Email)), sha256.Sum256([]byte(userCtx.IdToken)), e.dsInfo.UID)
|
||||
|
||||
conn, ok := e.dbConnections.Load(cacheKey)
|
||||
if ok {
|
||||
return conn.(*sql.DB), nil
|
||||
}
|
||||
|
||||
cnnstr, err := generateConnectionString(e.dsInfo, e.azureCredentials, e.kerberosAuth, e.log, e.azureSettings, userCtx.IdToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := newMSSQL(e.driverName, e.rowLimit, e.dsInfo, cnnstr, e.log, e.proxyClient)
|
||||
if err != nil {
|
||||
logger.Error("Failed connecting to MSSQL", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
e.dbConnections.Store(cacheKey, db)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
result := backend.NewQueryDataResponse()
|
||||
ch := make(chan DBDataResponse, len(req.Queries))
|
||||
@@ -293,7 +352,12 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := e.db.QueryContext(queryContext, interpolatedQuery)
|
||||
db, err := e.getDB(queryContext)
|
||||
if err != nil {
|
||||
errAppendDebug("retrieving database connection failed", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourcePlugin)
|
||||
return
|
||||
}
|
||||
rows, err := db.QueryContext(queryContext, interpolatedQuery)
|
||||
if err != nil {
|
||||
errAppendDebug("db query error", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream)
|
||||
return
|
||||
@@ -310,12 +374,19 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG
|
||||
return
|
||||
}
|
||||
|
||||
frame := e.processResponse(qm, rows, interpolatedQuery, errAppendDebug)
|
||||
|
||||
queryResult.dataResponse.Frames = data.Frames{frame}
|
||||
ch <- queryResult
|
||||
}
|
||||
|
||||
func (e *DataSourceHandler) processResponse(qm *dataQueryModel, rows *sql.Rows, interpolatedQuery string, errAppendDebug func(string, error, string, backend.ErrorSource)) *data.Frame {
|
||||
// Convert row.Rows to dataframe
|
||||
stringConverters := e.queryResultTransformer.GetConverterList()
|
||||
frame, err := sqlutil.FrameFromRows(rows, e.rowLimit, sqlutil.ToConverters(stringConverters...)...)
|
||||
if err != nil {
|
||||
errAppendDebug("convert frame from rows error", err, interpolatedQuery, backend.ErrorSourcePlugin)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if frame.Meta == nil {
|
||||
@@ -330,21 +401,19 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG
|
||||
// additionally-needed frame data stays intact and is correctly passed to our visulization.
|
||||
if frame.Rows() == 0 {
|
||||
frame.Fields = []*data.Field{}
|
||||
queryResult.dataResponse.Frames = data.Frames{frame}
|
||||
ch <- queryResult
|
||||
return
|
||||
return frame
|
||||
}
|
||||
|
||||
if err := convertSQLTimeColumnsToEpochMS(frame, qm); err != nil {
|
||||
errAppendDebug("converting time columns failed", err, interpolatedQuery, backend.ErrorSourcePlugin)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if qm.Format == dataQueryFormatSeries {
|
||||
// time series has to have time column
|
||||
if qm.timeIndex == -1 {
|
||||
errAppendDebug("db has no time column", errors.New("time column is missing; make sure your data includes a time column for time series format or switch to a table format that doesn't require it"), interpolatedQuery, backend.ErrorSourceDownstream)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure to name the time field 'Time' to be backward compatible with Grafana pre-v8.
|
||||
@@ -362,7 +431,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG
|
||||
var err error
|
||||
if frame, err = convertSQLValueColumnToFloat(frame, i); err != nil {
|
||||
errAppendDebug("convert value to float failed", err, interpolatedQuery, backend.ErrorSourcePlugin)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +442,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG
|
||||
frame, err = data.LongToWide(frame, qm.FillMissing)
|
||||
if err != nil {
|
||||
errAppendDebug("failed to convert long to wide series when converting from dataframe", err, interpolatedQuery, backend.ErrorSourcePlugin)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Before 8x, a special metric column was used to name time series. The LongToWide transforms that into a metric label on the value field.
|
||||
@@ -408,8 +477,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG
|
||||
}
|
||||
}
|
||||
|
||||
queryResult.dataResponse.Frames = data.Frames{frame}
|
||||
ch <- queryResult
|
||||
return frame
|
||||
}
|
||||
|
||||
// Interpolate provides global macros/substitutions for all sql datasources.
|
||||
|
||||
@@ -697,7 +697,7 @@ func TestGenerateConnectionString(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
connStr, err := generateConnectionString(tc.dataSource, "", false, nil, tc.kerberosCfg, logger)
|
||||
connStr, err := generateConnectionString(tc.dataSource, nil, tc.kerberosCfg, logger, nil, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expConnStr, connStr)
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
|
||||
const { dataSourceConfig: dsSettings, onChange } = props;
|
||||
const managedIdentityEnabled = config.azure.managedIdentityEnabled;
|
||||
const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled;
|
||||
const userIdentityEnabled = config.azure.userIdentityEnabled;
|
||||
|
||||
const credentials = useMemo(() => getCredentials(dsSettings), [dsSettings]);
|
||||
|
||||
@@ -33,6 +34,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
|
||||
<AzureCredentialsForm
|
||||
managedIdentityEnabled={managedIdentityEnabled}
|
||||
azureEntraPasswordCredentialsEnabled={azureEntraPasswordCredentialsEnabled}
|
||||
userIdentityEnabled={userIdentityEnabled}
|
||||
credentials={credentials}
|
||||
azureCloudOptions={KnownAzureClouds}
|
||||
onCredentialsChange={onCredentialsChange}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button, Field, Select, Input } from '@grafana/ui';
|
||||
export interface Props {
|
||||
managedIdentityEnabled: boolean;
|
||||
azureEntraPasswordCredentialsEnabled: boolean;
|
||||
userIdentityEnabled: boolean;
|
||||
credentials: AzureCredentials;
|
||||
azureCloudOptions?: SelectableValue[];
|
||||
onCredentialsChange: (updatedCredentials: AzureCredentials) => void;
|
||||
@@ -22,13 +23,25 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
azureCloudOptions,
|
||||
onCredentialsChange,
|
||||
disabled,
|
||||
userIdentityEnabled,
|
||||
} = props;
|
||||
|
||||
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
|
||||
const defaultAuthType = (() => {
|
||||
if (managedIdentityEnabled) {
|
||||
return 'msi';
|
||||
}
|
||||
|
||||
if (userIdentityEnabled) {
|
||||
return 'currentuser';
|
||||
}
|
||||
|
||||
return 'clientsecret';
|
||||
})();
|
||||
if (onCredentialsChange) {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
authType: selected.value || 'msi',
|
||||
authType: selected.value || defaultAuthType,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
@@ -131,6 +144,13 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
value: 'ad-password',
|
||||
label: t('azureauth.azure-credentials-form.auth-options-azure-entra', 'Azure Entra Password'),
|
||||
});
|
||||
|
||||
if (userIdentityEnabled) {
|
||||
authTypeOptions.unshift({
|
||||
value: 'currentuser',
|
||||
label: 'Current User',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user