MSSQL: Password auth for Azure AD (#89746)

* Password auth for Azure AD

* rename auth fields

* add azure flag for client password cred enabled

* prettier

* rename flag

* Update go.mod

* Update public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* Apply suggestions from code review

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

* update package

* go mod

* prettier

* remove password

* gowork

* remove unused env test

* linter

---------

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
pull/90454/head^2
Andrew Hackmann 12 months ago committed by GitHub
parent ac21fa8e18
commit 319a874033
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      conf/defaults.ini
  2. 4
      conf/sample.ini
  3. 6
      docs/sources/setup-grafana/configure-grafana/_index.md
  4. 2
      go.mod
  5. 2
      go.work.sum
  6. 2
      packages/grafana-runtime/src/config.ts
  7. 13
      pkg/api/dtos/frontend_settings.go
  8. 1
      pkg/api/frontendsettings.go
  9. 2
      pkg/services/pluginsintegration/pluginconfig/request.go
  10. 3
      pkg/services/pluginsintegration/pluginconfig/request_test.go
  11. 2
      pkg/setting/setting_azure.go
  12. 11
      pkg/tsdb/mssql/mssql.go
  13. 4
      public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts
  14. 2
      public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx
  15. 2
      public/app/plugins/datasource/mssql/azureauth/AzureCredentials.ts
  16. 41
      public/app/plugins/datasource/mssql/azureauth/AzureCredentialsConfig.ts
  17. 124
      public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx
  18. 2
      public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx
  19. 4
      public/app/plugins/datasource/mssql/types.ts

@ -997,6 +997,10 @@ username_assertion =
# By default this will include all Grafana Labs owned Azure plugins, or those that make use of Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL).
forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql
# Specifies whether Entra password auth can be used for the MSSQL data source
# Disabled by default, needs to be explicitly enabled
azure_entra_password_credentials_enabled = false
#################################### Role-based Access Control ###########
[rbac]
# If enabled, cache permissions in a in memory cache

@ -984,6 +984,10 @@
# By default this will include all Grafana Labs owned Azure plugins, or those that make use of Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL).
;forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql
# Specifies whether Entra password auth can be used for the MSSQL data source
# Disabled by default, needs to be explicitly enabled
;azure_entra_password_credentials_enabled = false
#################################### Role-based Access Control ###########
[rbac]
;permission_cache = true

@ -1275,6 +1275,12 @@ Set plugins that will receive Azure settings via plugin context.
By default, this will include all Grafana Labs owned Azure plugins or those that use Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL).
### azure_entra_password_credentials_enabled
Specifies whether Entra password auth can be used for the MSSQL data source. This authentication is not recommended and consideration should be taken before enabling this.
Disabled by default, needs to be explicitly enabled.
## [auth.jwt]
Refer to [JWT authentication]({{< relref "../configure-security/configure-authentication/jwt" >}}) for more information.

@ -88,7 +88,7 @@ require (
github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana-aws-sdk v0.28.0 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go/v2 v2.0.4 // @grafana/partner-datasources
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0 // @grafana/partner-datasources
github.com/grafana/grafana-cloud-migration-snapshot v1.1.0 // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group

@ -416,6 +416,8 @@ github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw
github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY=
github.com/grafana/e2e v0.1.1 h1:/b6xcv5BtoBnx8cZnCiey9DbjEc8z7gXHO5edoeRYxc=
github.com/grafana/e2e v0.1.1/go.mod h1:RpNLgae5VT+BUHvPE+/zSypmOXKwEu4t+tnEMS1ATaE=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0 h1:lajVqTWaE96MpbjZToj7EshvqgRWOfYNkD4MbIZizaY=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0/go.mod h1:aKlFPE36IDa8qccRg3KbgZX3MQ5xymS3RelT4j6kkVU=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b h1:HCbWyVL6vi7gxyO76gQksSPH203oBJ1MJ3JcG1OQlsg=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=

@ -26,6 +26,7 @@ export interface AzureSettings {
workloadIdentityEnabled: boolean;
userIdentityEnabled: boolean;
userIdentityFallbackCredentialsEnabled: boolean;
azureEntraPasswordCredentialsEnabled: boolean;
}
export interface AzureCloudInfo {
@ -131,6 +132,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
workloadIdentityEnabled: false,
userIdentityEnabled: false,
userIdentityFallbackCredentialsEnabled: false,
azureEntraPasswordCredentialsEnabled: false,
};
caching = {
enabled: false,

@ -66,12 +66,13 @@ type FrontendSettingsLicenseInfoDTO struct {
}
type FrontendSettingsAzureDTO struct {
Cloud string `json:"cloud"`
Clouds []azsettings.AzureCloudInfo `json:"clouds"`
ManagedIdentityEnabled bool `json:"managedIdentityEnabled"`
WorkloadIdentityEnabled bool `json:"workloadIdentityEnabled"`
UserIdentityEnabled bool `json:"userIdentityEnabled"`
UserIdentityFallbackCredentialsEnabled bool `json:"userIdentityFallbackCredentialsEnabled"`
Cloud string `json:"cloud,omitempty"`
Clouds []azsettings.AzureCloudInfo `json:"clouds,omitempty"`
ManagedIdentityEnabled bool `json:"managedIdentityEnabled,omitempty"`
WorkloadIdentityEnabled bool `json:"workloadIdentityEnabled,omitempty"`
UserIdentityEnabled bool `json:"userIdentityEnabled,omitempty"`
UserIdentityFallbackCredentialsEnabled bool `json:"userIdentityFallbackCredentialsEnabled,omitempty"`
AzureEntraPasswordCredentialsEnabled bool `json:"azureEntraPasswordCredentialsEnabled,omitempty"`
}
type FrontendSettingsCachingDTO struct {

@ -276,6 +276,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
WorkloadIdentityEnabled: hs.Cfg.Azure.WorkloadIdentityEnabled,
UserIdentityEnabled: hs.Cfg.Azure.UserIdentityEnabled,
UserIdentityFallbackCredentialsEnabled: hs.Cfg.Azure.UserIdentityFallbackCredentialsEnabled,
AzureEntraPasswordCredentialsEnabled: hs.Cfg.Azure.AzureEntraPasswordCredentialsEnabled,
},
Caching: dtos.FrontendSettingsCachingDTO{

@ -142,6 +142,8 @@ func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginI
}
}
}
m[azsettings.AzureEntraPasswordCredentialsEnabled] = strconv.FormatBool(azureSettings.AzureEntraPasswordCredentialsEnabled)
}
if s.cfg.UserFacingDefaultError != "" {

@ -309,6 +309,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
},
UserIdentityFallbackCredentialsEnabled: true,
ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"},
AzureEntraPasswordCredentialsEnabled: true,
}
t.Run("uses the azure settings for an Azure plugin", func(t *testing.T) {
@ -389,6 +390,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_ID")
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_SECRET")
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ASSERTION")
require.NotContains(t, m, "GFAZPL_AZURE_ENTRA_PASSWORD_CREDENTIALS_ENABLED")
})
t.Run("uses the azure settings for a non-Azure user-specified plugin", func(t *testing.T) {
@ -413,6 +415,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
"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_AZURE_ENTRA_PASSWORD_CREDENTIALS_ENABLED": "true",
})
})
}

@ -80,5 +80,7 @@ func (cfg *Cfg) readAzureSettings() {
azureSettings.ForwardSettingsPlugins = util.SplitString(azureSection.Key("forward_settings_to_plugins").String())
azureSettings.AzureEntraPasswordCredentialsEnabled = azureSection.Key("azure_entra_password_credentials_enabled").MustBool(false)
cfg.Azure = azureSettings
}

@ -321,6 +321,17 @@ func getAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia
c.ClientSecret,
"ActiveDirectoryApplication",
)
case *azcredentials.AzureEntraPasswordCredentials:
if cfg.Azure.AzureEntraPasswordCredentialsEnabled {
connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;",
c.UserId,
c.Password,
c.ClientId,
"ActiveDirectoryPassword",
)
} else {
return "", fmt.Errorf("azure entra password authentication is not enabled")
}
default:
return "", fmt.Errorf("unsupported azure authentication type")
}

@ -9,6 +9,7 @@ export const configWithManagedIdentityEnabled: Partial<GrafanaBootConfig> = {
workloadIdentityEnabled: false,
userIdentityEnabled: false,
userIdentityFallbackCredentialsEnabled: false,
azureEntraPasswordCredentialsEnabled: false,
},
};
@ -19,6 +20,7 @@ export const configWithManagedIdentityDisabled: Partial<GrafanaBootConfig> = {
userIdentityEnabled: false,
cloud: 'AzureCloud',
userIdentityFallbackCredentialsEnabled: false,
azureEntraPasswordCredentialsEnabled: false,
},
};
@ -48,5 +50,5 @@ export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial<
DataSourceSettings<AzureAuthJSONDataType, AzureAuthSecureJSONDataType>
> = {
...basicJSONData,
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' },
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX', password: undefined },
};

@ -13,6 +13,7 @@ import { AzureCredentialsForm } from './AzureCredentialsForm';
export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
const { dataSourceConfig: dsSettings, onChange } = props;
const managedIdentityEnabled = config.azure.managedIdentityEnabled;
const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled;
const credentials = useMemo(() => getCredentials(dsSettings, config), [dsSettings]);
@ -30,6 +31,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
return (
<AzureCredentialsForm
managedIdentityEnabled={managedIdentityEnabled}
azureEntraPasswordCredentialsEnabled={azureEntraPasswordCredentialsEnabled}
credentials={credentials}
azureCloudOptions={KnownAzureClouds}
onCredentialsChange={onCredentialsChange}

@ -15,5 +15,7 @@ export function isCredentialsComplete(credentials: AzureCredentialsType): boolea
return true;
case AzureAuthType.CLIENT_SECRET:
return !!(credentials.azureCloud && credentials.tenantId && credentials.clientId && credentials.clientSecret);
case AzureAuthType.AD_PASSWORD:
return !!(credentials.clientId && credentials.password && credentials.userId);
}
}

@ -19,15 +19,15 @@ export const getDefaultCredentials = (managedIdentityEnabled: boolean, cloud: st
};
export const getSecret = (
clientSecretStoredServerSide: boolean,
clientSecret: string | symbol | undefined
storedServerSide: boolean,
secret: string | symbol | undefined
): undefined | string | ConcealedSecretType => {
const concealedSecret: ConcealedSecretType = Symbol('Concealed client secret');
if (clientSecretStoredServerSide) {
if (storedServerSide) {
// The secret is concealed server side, so return the symbol
return concealedSecret;
} else {
return typeof clientSecret === 'string' && clientSecret.length > 0 ? clientSecret : undefined;
return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
}
};
@ -41,6 +41,8 @@ export const getCredentials = (
// Secure JSON data/fields
const clientSecretStoredServerSide = dsSettings.secureJsonFields?.azureClientSecret;
const clientSecret = dsSettings.secureJsonData?.azureClientSecret;
const passwordStoredServerSide = dsSettings.secureJsonFields?.password;
const password = dsSettings.secureJsonData?.password;
// BootConfig data
const managedIdentityEnabled = !!bootConfig.azure?.managedIdentityEnabled;
@ -74,6 +76,13 @@ export const getCredentials = (
clientId: credentials.clientId,
clientSecret: getSecret(clientSecretStoredServerSide, clientSecret),
};
case AzureAuthType.AD_PASSWORD:
return {
authType: AzureAuthType.AD_PASSWORD,
userId: credentials.userId,
clientId: credentials.clientId,
password: getSecret(passwordStoredServerSide, password),
};
}
};
@ -130,5 +139,29 @@ export const updateCredentials = (
};
return dsSettings;
case AzureAuthType.AD_PASSWORD:
return {
...dsSettings,
jsonData: {
...dsSettings.jsonData,
azureCredentials: {
authType: AzureAuthType.AD_PASSWORD,
userId: credentials.userId,
clientId: credentials.clientId,
},
},
secureJsonData: {
...dsSettings.secureJsonData,
password:
typeof credentials.password === 'string' && credentials.password.length > 0
? credentials.password
: undefined,
},
secureJsonFields: {
...dsSettings.secureJsonFields,
password: typeof credentials.password === 'symbol',
},
};
}
};

@ -7,25 +7,22 @@ import { AzureCredentialsType, AzureAuthType } from '../types';
export interface Props {
managedIdentityEnabled: boolean;
azureEntraPasswordCredentialsEnabled: boolean;
credentials: AzureCredentialsType;
azureCloudOptions?: SelectableValue[];
onCredentialsChange: (updatedCredentials: AzureCredentialsType) => void;
disabled?: boolean;
}
const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
{
value: AzureAuthType.MSI,
label: 'Managed Identity',
},
{
value: AzureAuthType.CLIENT_SECRET,
label: 'App Registration',
},
];
export const AzureCredentialsForm = (props: Props) => {
const { managedIdentityEnabled, credentials, azureCloudOptions, onCredentialsChange, disabled } = props;
const {
managedIdentityEnabled,
azureEntraPasswordCredentialsEnabled,
credentials,
azureCloudOptions,
onCredentialsChange,
disabled,
} = props;
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
if (onCredentialsChange) {
@ -37,8 +34,27 @@ export const AzureCredentialsForm = (props: Props) => {
}
};
const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
{
value: AzureAuthType.CLIENT_SECRET,
label: 'App Registration',
},
];
if (managedIdentityEnabled) {
authTypeOptions.push({
value: AzureAuthType.MSI,
label: 'Managed Identity',
});
}
if (azureEntraPasswordCredentialsEnabled) {
authTypeOptions.push({
value: AzureAuthType.AD_PASSWORD,
label: 'Azure Entra Password',
});
}
const onInputChange = ({ property, value }: { property: keyof AzureCredentialsType; value: string }) => {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
if (onCredentialsChange) {
const updated: AzureCredentialsType = {
...credentials,
[property]: value,
@ -49,7 +65,6 @@ export const AzureCredentialsForm = (props: Props) => {
return (
<div>
{managedIdentityEnabled && (
<Field
label="Authentication"
description="Choose the type of authentication to Azure services"
@ -63,8 +78,7 @@ export const AzureCredentialsForm = (props: Props) => {
disabled={disabled}
/>
</Field>
)}
{credentials.authType === 'clientsecret' && (
{credentials.authType === AzureAuthType.CLIENT_SECRET && (
<>
{azureCloudOptions && (
<Field label="Azure Cloud" htmlFor="azure-cloud-type" disabled={disabled}>
@ -167,6 +181,84 @@ export const AzureCredentialsForm = (props: Props) => {
))}
</>
)}
{credentials.authType === AzureAuthType.AD_PASSWORD && azureEntraPasswordCredentialsEnabled && (
<>
<Field label="User Id" required htmlFor="user-id" invalid={!credentials.userId} error={'User ID is required'}>
<Input
width={45}
value={credentials.userId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'userId', value });
}}
disabled={disabled}
aria-label="User ID"
/>
</Field>
<Field
label="Application Client ID"
required
htmlFor="application-client-id"
invalid={!credentials.clientId}
error={'Application Client ID is required'}
>
<Input
width={45}
value={credentials.clientId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'clientId', value });
}}
disabled={disabled}
aria-label="Application Client ID"
/>
</Field>
{!disabled &&
(typeof credentials.password === 'symbol' ? (
<Field label="Password" htmlFor="password" required>
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
<Input
aria-label="Password"
placeholder="configured"
disabled={true}
data-testid={'password'}
width={45}
/>
<Button
variant="secondary"
type="button"
onClick={() => {
onInputChange({ property: 'password', value: '' });
}}
disabled={disabled}
>
Reset
</Button>
</div>
</Field>
) : (
<Field
label="Password"
required
htmlFor="password"
invalid={!credentials.password}
error={'Password is required'}
>
<Input
width={45}
aria-label="Password"
value={credentials.password || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'password', value });
}}
id="password"
disabled={disabled}
/>
</Field>
))}
</>
)}
</div>
);
};

@ -108,7 +108,7 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
if (azureAuthIsSupported) {
return [
...basicAuthenticationOptions,
{ value: MSSQLAuthenticationType.azureAuth, label: 'Azure AD Authentication' },
{ value: MSSQLAuthenticationType.azureAuth, label: MSSQLAuthenticationType.azureAuth },
];
}

@ -28,6 +28,7 @@ export type ConcealedSecretType = symbol;
export enum AzureAuthType {
MSI = 'msi',
CLIENT_SECRET = 'clientsecret',
AD_PASSWORD = 'ad-password',
}
export interface AzureCredentialsType {
@ -36,6 +37,8 @@ export interface AzureCredentialsType {
tenantId?: string;
clientId?: string;
clientSecret?: string | ConcealedSecretType;
userId?: string;
password?: string | ConcealedSecretType;
}
export interface MssqlOptions extends SQLOptions {
@ -63,6 +66,7 @@ export type AzureAuthJSONDataType = DataSourceJsonData & {
export type AzureAuthSecureJSONDataType = {
azureClientSecret: undefined | string | ConcealedSecretType;
password: undefined | string | ConcealedSecretType;
};
export type AzureAuthConfigType = {

Loading…
Cancel
Save