Skip to content

Commit 38bf447

Browse files
committed
Rewrite credential handling
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
1 parent 5c11907 commit 38bf447

File tree

9 files changed

+163
-76
lines changed

9 files changed

+163
-76
lines changed

.changeset/serious-badgers-fetch.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'grafana-infinity-datasource': minor
33
---
44

5-
Add native Azure authentication
5+
Add native Microsoft authentication

docker-compose.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ services:
2222
- GF_SECURITY_ANGULAR_SUPPORT_ENABLED=false
2323
- GF_SECURITY_CSRF_ALWAYS_CHECK=true
2424
- GF_ENTERPRISE_LICENSE_TEXT=$GF_ENTERPRISE_LICENSE_TEXT
25+
- GF_AZURE_FORWARD_SETTINGS_TO_PLUGINS=
26+
- GF_AZURE_WORKLOAD_IDENTITY_ENABLED=true
27+
- GF_AZURE_USER_IDENTITY_ENABLED=true

pkg/infinity/azure.go

+33-27
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7-
"os"
7+
"strings"
88

99
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
1010
"github.com/grafana/grafana-azure-sdk-go/azhttpclient"
@@ -19,56 +19,62 @@ func ApplyAzureAuth(ctx context.Context, httpClient *http.Client, settings model
1919
defer span.End()
2020

2121
if IsAzureAuthConfigured(settings) {
22-
azSettings := &azsettings.AzureSettings{
23-
Cloud: string(settings.MicrosoftSettings.Cloud),
24-
ManagedIdentityEnabled: true,
25-
WorkloadIdentityEnabled: true,
26-
}
27-
28-
tenantID := settings.MicrosoftSettings.TenantID
29-
if tenantID == "" {
30-
tenantID = os.Getenv("AZURE_TENANT_ID")
31-
}
32-
33-
clientID := settings.OAuth2Settings.ClientID
34-
if clientID == "" {
35-
clientID = os.Getenv("AZURE_CLIENT_ID")
22+
azSettings, err := azsettings.ReadFromEnv()
23+
if err != nil {
24+
return nil, err
3625
}
3726

3827
var credentials azcredentials.AzureCredentials
3928

4029
switch settings.MicrosoftSettings.AuthType {
4130
case models.MicrosoftAuthTypeClientSecret:
42-
clientSecret := settings.OAuth2Settings.ClientSecret
43-
if clientSecret == "" {
44-
clientSecret = os.Getenv("AZURE_CLIENT_SECRET")
31+
32+
if strings.TrimSpace(settings.MicrosoftSettings.TenantID) == "" {
33+
return nil, fmt.Errorf("Tenant ID %w ", models.MicrosoftRequiredForClientSecretErrHelp)
34+
}
35+
36+
if strings.TrimSpace(settings.MicrosoftSettings.ClientID) == "" {
37+
return nil, fmt.Errorf("Client ID %w ", models.MicrosoftRequiredForClientSecretErrHelp)
38+
}
39+
40+
if strings.TrimSpace(settings.MicrosoftSettings.ClientSecret) == "" {
41+
return nil, fmt.Errorf("Client secret %w ", models.MicrosoftRequiredForClientSecretErrHelp)
4542
}
4643

4744
credentials = &azcredentials.AzureClientSecretCredentials{
4845
AzureCloud: string(settings.MicrosoftSettings.Cloud),
49-
TenantId: tenantID,
50-
ClientId: clientID,
51-
ClientSecret: clientSecret,
46+
TenantId: settings.MicrosoftSettings.TenantID,
47+
ClientId: settings.MicrosoftSettings.ClientID,
48+
ClientSecret: settings.MicrosoftSettings.ClientSecret,
5249
}
5350
case models.MicrosoftAuthTypeManagedIdentity:
54-
azSettings.ManagedIdentityClientId = clientID
51+
if !azSettings.ManagedIdentityEnabled {
52+
return nil, fmt.Errorf("Managed Identity %w ", models.MicrosoftDisabledAuthErrHelp)
53+
}
5554

5655
credentials = &azcredentials.AzureManagedIdentityCredentials{
57-
ClientId: clientID,
56+
// ClientId is optional for managed identity, because it can be inferred from the environment
57+
// https://github.com/grafana/grafana-azure-sdk-go/blob/21e2891b4190eb7c255c8cd275836def8200faf8/aztokenprovider/retriever_msi.go#L20-L30
58+
ClientId: settings.MicrosoftSettings.ClientID,
5859
}
5960
case models.MicrosoftAuthTypeWorkloadIdentity:
60-
azSettings.WorkloadIdentitySettings = &azsettings.WorkloadIdentitySettings{
61-
TenantId: tenantID,
62-
ClientId: clientID,
61+
if !azSettings.WorkloadIdentityEnabled {
62+
return nil, fmt.Errorf("Workload Identity %w ", models.MicrosoftDisabledAuthErrHelp)
6363
}
6464

6565
credentials = &azcredentials.AzureWorkloadIdentityCredentials{}
66+
case models.MicrosoftAuthTypeCurrentUserIdentity:
67+
if !azSettings.UserIdentityEnabled {
68+
return nil, fmt.Errorf("User Identity %w ", models.MicrosoftDisabledAuthErrHelp)
69+
}
70+
71+
credentials = &azcredentials.AadCurrentUserCredentials{}
6672
default:
6773
panic(fmt.Errorf("invalid auth type '%s'", settings.MicrosoftSettings.AuthType))
6874
}
6975

7076
authOpts := azhttpclient.NewAuthOptions(azSettings)
71-
authOpts.Scopes(settings.OAuth2Settings.Scopes)
77+
authOpts.Scopes(settings.MicrosoftSettings.Scopes)
7278

7379
httpClient.Transport = azhttpclient.AzureMiddleware(authOpts, credentials).
7480
CreateMiddleware(httpclient.Options{}, httpClient.Transport)

pkg/infinity/client.go

+6-8
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func NewClient(ctx context.Context, settings models.InfinitySettings) (client *C
159159
}
160160

161161
func ApplySecureSocksProxyConfiguration(httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) {
162-
if IsAwsAuthConfigured(settings) {
162+
if IsAwsAuthConfigured(settings) || IsAzureAuthConfigured(settings) {
163163
return httpClient, nil
164164
}
165165
t := httpClient.Transport
@@ -171,13 +171,11 @@ func ApplySecureSocksProxyConfiguration(httpClient *http.Client, settings models
171171
t = t.(*oauth2.Transport).Base
172172
}
173173

174-
if t, ok := t.(*http.Transport); ok {
175-
// secure socks proxy configuration - checks if enabled inside the function
176-
err := proxy.New(settings.ProxyOpts.ProxyOptions).ConfigureSecureSocksHTTPProxy(t)
177-
if err != nil {
178-
backend.Logger.Error("error configuring secure socks proxy", "err", err.Error())
179-
return nil, fmt.Errorf("error configuring secure socks proxy. %s", err)
180-
}
174+
// secure socks proxy configuration - checks if enabled inside the function
175+
err := proxy.New(settings.ProxyOpts.ProxyOptions).ConfigureSecureSocksHTTPProxy(t.(*http.Transport))
176+
if err != nil {
177+
backend.Logger.Error("error configuring secure socks proxy", "err", err.Error())
178+
return nil, fmt.Errorf("error configuring secure socks proxy. %s", err)
181179
}
182180

183181
return httpClient, nil

pkg/models/settings.go

+56-7
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ type AWSSettings struct {
6868
type MicrosoftAuthType string
6969

7070
const (
71-
MicrosoftAuthTypeManagedIdentity MicrosoftAuthType = azcredentials.AzureAuthManagedIdentity
72-
MicrosoftAuthTypeWorkloadIdentity MicrosoftAuthType = azcredentials.AzureAuthWorkloadIdentity
73-
MicrosoftAuthTypeClientSecret MicrosoftAuthType = azcredentials.AzureAuthClientSecret
71+
MicrosoftAuthTypeManagedIdentity MicrosoftAuthType = azcredentials.AzureAuthManagedIdentity
72+
MicrosoftAuthTypeWorkloadIdentity MicrosoftAuthType = azcredentials.AzureAuthWorkloadIdentity
73+
MicrosoftAuthTypeClientSecret MicrosoftAuthType = azcredentials.AzureAuthClientSecret
74+
MicrosoftAuthTypeCurrentUserIdentity MicrosoftAuthType = azcredentials.AzureAuthCurrentUserIdentity
7475
)
7576

7677
type MicrosoftCloudType string
@@ -81,10 +82,20 @@ const (
8182
MicrosoftCloudUSGovernment MicrosoftCloudType = azsettings.AzureUSGovernment
8283
)
8384

85+
var (
86+
MicrosoftRequiredForClientSecretErrHelp = errors.New(` is required for Microsoft client secret authentication`)
87+
MicrosoftDisabledAuthErrHelp = errors.New(` is not enabled in the Grafana Azure settings. For more information, please refer to the Grafana documentation at
88+
https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure.
89+
Additionally, this plugin needs to be added to the grafana.ini setting azure.forward_settings_to_plugins.`)
90+
)
91+
8492
type MicrosoftSettings struct {
85-
Cloud MicrosoftCloudType `json:"cloud"`
86-
AuthType MicrosoftAuthType `json:"auth_type"`
87-
TenantID string `json:"tenant_id"`
93+
Cloud MicrosoftCloudType `json:"cloud"`
94+
AuthType MicrosoftAuthType `json:"auth_type"`
95+
TenantID string `json:"tenant_id"`
96+
ClientID string `json:"client_id"`
97+
ClientSecret string
98+
Scopes []string `json:"scopes,omitempty"`
8899
}
89100

90101
type ProxyType string
@@ -165,6 +176,42 @@ func (s *InfinitySettings) Validate() error {
165176
}
166177
return nil
167178
}
179+
if s.AuthenticationMethod == AuthenticationMethodMicrosoft {
180+
azSettings, err := azsettings.ReadFromEnv()
181+
if err != nil {
182+
return err
183+
}
184+
185+
switch s.MicrosoftSettings.AuthType {
186+
case MicrosoftAuthTypeClientSecret:
187+
if strings.TrimSpace(s.MicrosoftSettings.TenantID) == "" {
188+
return fmt.Errorf("Tenant ID %w ", MicrosoftRequiredForClientSecretErrHelp)
189+
}
190+
191+
if strings.TrimSpace(s.MicrosoftSettings.ClientID) == "" {
192+
return fmt.Errorf("Client ID %w ", MicrosoftRequiredForClientSecretErrHelp)
193+
}
194+
195+
if strings.TrimSpace(s.MicrosoftSettings.ClientSecret) == "" {
196+
return fmt.Errorf("Client secret %w ", MicrosoftRequiredForClientSecretErrHelp)
197+
}
198+
case MicrosoftAuthTypeManagedIdentity:
199+
if !azSettings.ManagedIdentityEnabled {
200+
return errors.New("managed identity authentication is not enabled in Grafana config. " +
201+
"Refer https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure")
202+
}
203+
case MicrosoftAuthTypeWorkloadIdentity:
204+
if !azSettings.WorkloadIdentityEnabled {
205+
return errors.New("workload identity authentication is not enabled in Grafana config." +
206+
"Refer https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure")
207+
}
208+
case MicrosoftAuthTypeCurrentUserIdentity:
209+
if !azSettings.UserIdentityEnabled {
210+
return errors.New("user identity authentication is not enabled in Grafana config." +
211+
"Refer https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure")
212+
}
213+
}
214+
}
168215
if s.AuthenticationMethod != AuthenticationMethodNone && len(s.AllowedHosts) < 1 {
169216
return errors.New("configure allowed hosts in the authentication section")
170217
}
@@ -212,7 +259,6 @@ type InfinitySettingsJson struct {
212259
ProxyType ProxyType `json:"proxy_type,omitempty"`
213260
ProxyUrl string `json:"proxy_url,omitempty"`
214261
AllowedHosts []string `json:"allowedHosts,omitempty"`
215-
216262
ReferenceData []RefData `json:"refData,omitempty"`
217263
CustomHealthCheckEnabled bool `json:"customHealthCheckEnabled,omitempty"`
218264
CustomHealthCheckUrl string `json:"customHealthCheckUrl,omitempty"`
@@ -312,6 +358,9 @@ func LoadSettings(ctx context.Context, config backend.DataSourceInstanceSettings
312358
if val, ok := config.DecryptedSecureJSONData["azureBlobAccountKey"]; ok {
313359
settings.AzureBlobAccountKey = val
314360
}
361+
if val, ok := config.DecryptedSecureJSONData["microsoftClientSecret"]; ok {
362+
settings.MicrosoftSettings.ClientSecret = val
363+
}
315364
settings.CustomHeaders = GetSecrets(config, "httpHeaderName", "httpHeaderValue")
316365
settings.SecureQueryFields = GetSecrets(config, "secureQueryName", "secureQueryValue")
317366
settings.OAuth2Settings.EndpointParams = GetSecrets(config, "oauth2EndPointParamsName", "oauth2EndPointParamsValue")

pkg/models/settings_test.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,11 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) {
161161
"service" : "service1"
162162
},
163163
"microsoft" : {
164-
"cloud" : "AzureUSGovernment",
165-
"auth_type" : "clientsecret",
166-
"tenant_id" : "tenant1"
164+
"cloud" : "AzureUSGovernment",
165+
"auth_type" : "clientsecret",
166+
"tenant_id" : "tenant1",
167+
"client_id" : "myMicrosoftClientID",
168+
"scopes" : ["msscope1","msscope2"]
167169
},
168170
"oauth2" : {
169171
"client_id":"myClientID",
@@ -186,6 +188,7 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) {
186188
"awsAccessKey": "awsAccessKey1",
187189
"awsSecretKey": "awsSecretKey1",
188190
"oauth2ClientSecret": "myOauth2ClientSecret",
191+
"microsoftClientSecret": "myMicrosoftClientSecret",
189192
"oauth2JWTPrivateKey": "myOauth2JWTPrivateKey",
190193
"oauth2EndPointParamsValue1": "Resource1",
191194
"oauth2EndPointParamsValue2": "Resource2",
@@ -218,9 +221,12 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) {
218221
Region: "region1",
219222
},
220223
MicrosoftSettings: models.MicrosoftSettings{
221-
Cloud: models.MicrosoftCloudUSGovernment,
222-
AuthType: models.MicrosoftAuthTypeClientSecret,
223-
TenantID: "tenant1",
224+
Cloud: models.MicrosoftCloudUSGovernment,
225+
AuthType: models.MicrosoftAuthTypeClientSecret,
226+
TenantID: "tenant1",
227+
ClientID: "myMicrosoftClientID",
228+
ClientSecret: "myMicrosoftClientSecret",
229+
Scopes: []string{"msscope1", "msscope2"},
224230
},
225231
OAuth2Settings: models.OAuth2Settings{
226232
ClientID: "myClientID",

src/editors/config/Auth.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const authTypes: Array<SelectableValue<AuthType | 'others'> & { logo?: string }>
1818
{ value: 'oauthPassThru', label: 'Forward OAuth' },
1919
{ value: 'oauth2', label: 'OAuth2', logo: '/public/plugins/yesoreyeram-infinity-datasource/img/oauth-2-sm.png' },
2020
{ value: 'aws', label: 'AWS', logo: '/public/plugins/yesoreyeram-infinity-datasource/img/aws.jpg' },
21-
{ value: 'microsoft', label: 'Microsoft Entra ID' },
21+
{ value: 'microsoft', label: 'Microsoft Entra ID', logo: '/public/img/microsoft_auth_icon.svg' },
2222
{ value: 'azureBlob', label: 'Azure Blob' },
2323
{ value: 'others', label: 'Other Auth Providers' },
2424
];
@@ -137,7 +137,7 @@ export const AuthEditor = (props: DataSourcePluginOptionsEditorProps<InfinityOpt
137137
e.preventDefault();
138138
}}
139139
>
140-
{a.logo ? <img src={a.logo} width={24} height={24} style={{ marginInlineEnd: '10px' }} /> : <Icon name="key-skeleton-alt" style={{ marginInlineEnd: '10px' }} />}
140+
{a.logo ? <img src={a.logo} width={16} height={16} style={{ marginInlineEnd: '10px' }} /> : <Icon name="key-skeleton-alt" style={{ marginInlineEnd: '10px' }} />}
141141
{a.label}
142142
</a>
143143
))}

0 commit comments

Comments
 (0)