mirror of https://github.com/grafana/grafana
Infra: Azure authentication in HttpClientProvider (#36932)
* Azure middleware in HttpClientProxy * Azure authentication under feature flag * Minor fixes * Add prefixes to not clash with JsonData * Return error if JsonData cannot be parsed * Return original string if URL invalid * Tests for datasource_cachepull/36932/merge
parent
4664cba935
commit
c1963024ec
@ -0,0 +1,113 @@ |
||||
package httpclientprovider |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"path" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials" |
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/aztokenprovider" |
||||
) |
||||
|
||||
const azureMiddlewareName = "AzureAuthentication.Provider" |
||||
|
||||
func AzureMiddleware(cfg *setting.Cfg) httpclient.Middleware { |
||||
return httpclient.NamedMiddlewareFunc(azureMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper { |
||||
if enabled, err := isAzureAuthenticationEnabled(opts.CustomOptions); err != nil { |
||||
return errorResponse(err) |
||||
} else if !enabled { |
||||
return next |
||||
} |
||||
|
||||
credentials, err := getAzureCredentials(opts.CustomOptions) |
||||
if err != nil { |
||||
return errorResponse(err) |
||||
} else if credentials == nil { |
||||
credentials = getDefaultAzureCredentials(cfg) |
||||
} |
||||
|
||||
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, credentials) |
||||
if err != nil { |
||||
return errorResponse(err) |
||||
} |
||||
|
||||
scopes, err := getAzureEndpointScopes(opts.CustomOptions) |
||||
if err != nil { |
||||
return errorResponse(err) |
||||
} |
||||
|
||||
return aztokenprovider.ApplyAuth(tokenProvider, scopes, next) |
||||
}) |
||||
} |
||||
|
||||
func errorResponse(err error) http.RoundTripper { |
||||
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { |
||||
return nil, fmt.Errorf("invalid Azure configuration: %s", err) |
||||
}) |
||||
} |
||||
|
||||
func isAzureAuthenticationEnabled(customOptions map[string]interface{}) (bool, error) { |
||||
if untypedValue, ok := customOptions["_azureAuth"]; !ok { |
||||
return false, nil |
||||
} else if value, ok := untypedValue.(bool); !ok { |
||||
err := fmt.Errorf("the field 'azureAuth' should be a bool") |
||||
return false, err |
||||
} else { |
||||
return value, nil |
||||
} |
||||
} |
||||
|
||||
func getAzureCredentials(customOptions map[string]interface{}) (azcredentials.AzureCredentials, error) { |
||||
if untypedValue, ok := customOptions["_azureCredentials"]; !ok { |
||||
return nil, nil |
||||
} else if value, ok := untypedValue.(azcredentials.AzureCredentials); !ok { |
||||
err := fmt.Errorf("the field 'azureCredentials' should be a valid credentials object") |
||||
return nil, err |
||||
} else { |
||||
return value, nil |
||||
} |
||||
} |
||||
|
||||
func getDefaultAzureCredentials(cfg *setting.Cfg) azcredentials.AzureCredentials { |
||||
if cfg.Azure.ManagedIdentityEnabled { |
||||
return &azcredentials.AzureManagedIdentityCredentials{} |
||||
} else { |
||||
return &azcredentials.AzureClientSecretCredentials{ |
||||
AzureCloud: cfg.Azure.Cloud, |
||||
} |
||||
} |
||||
} |
||||
|
||||
func getAzureEndpointResourceId(customOptions map[string]interface{}) (*url.URL, error) { |
||||
var value string |
||||
if untypedValue, ok := customOptions["azureEndpointResourceId"]; !ok { |
||||
err := fmt.Errorf("the field 'azureEndpointResourceId' should be set") |
||||
return nil, err |
||||
} else if value, ok = untypedValue.(string); !ok { |
||||
err := fmt.Errorf("the field 'azureEndpointResourceId' should be a string") |
||||
return nil, err |
||||
} |
||||
|
||||
resourceId, err := url.Parse(value) |
||||
if err != nil || resourceId.Scheme == "" || resourceId.Host == "" { |
||||
err := fmt.Errorf("invalid endpoint Resource ID URL '%s'", value) |
||||
return nil, err |
||||
} |
||||
|
||||
return resourceId, nil |
||||
} |
||||
|
||||
func getAzureEndpointScopes(customOptions map[string]interface{}) ([]string, error) { |
||||
resourceId, err := getAzureEndpointResourceId(customOptions) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
resourceId.Path = path.Join(resourceId.Path, ".default") |
||||
scopes := []string{resourceId.String()} |
||||
|
||||
return scopes, nil |
||||
} |
@ -0,0 +1,83 @@ |
||||
package azcredentials |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
func FromDatasourceData(data map[string]interface{}, secureData map[string]string) (AzureCredentials, error) { |
||||
if credentialsObj, err := getMapOptional(data, "azureCredentials"); err != nil { |
||||
return nil, err |
||||
} else if credentialsObj == nil { |
||||
return nil, nil |
||||
} else { |
||||
return getFromCredentialsObject(credentialsObj, secureData) |
||||
} |
||||
} |
||||
|
||||
func getFromCredentialsObject(credentialsObj map[string]interface{}, secureData map[string]string) (AzureCredentials, error) { |
||||
authType, err := getStringValue(credentialsObj, "authType") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
switch authType { |
||||
case AzureAuthManagedIdentity: |
||||
credentials := &AzureManagedIdentityCredentials{} |
||||
return credentials, nil |
||||
|
||||
case AzureAuthClientSecret: |
||||
cloud, err := getStringValue(credentialsObj, "azureCloud") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tenantId, err := getStringValue(credentialsObj, "tenantId") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
clientId, err := getStringValue(credentialsObj, "clientId") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
clientSecret := secureData["azureClientSecret"] |
||||
|
||||
credentials := &AzureClientSecretCredentials{ |
||||
AzureCloud: cloud, |
||||
TenantId: tenantId, |
||||
ClientId: clientId, |
||||
ClientSecret: clientSecret, |
||||
} |
||||
return credentials, nil |
||||
|
||||
default: |
||||
err := fmt.Errorf("the authentication type '%s' not supported", authType) |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
func getMapOptional(obj map[string]interface{}, key string) (map[string]interface{}, error) { |
||||
if untypedValue, ok := obj[key]; ok { |
||||
if value, ok := untypedValue.(map[string]interface{}); ok { |
||||
return value, nil |
||||
} else { |
||||
err := fmt.Errorf("the field '%s' should be an object", key) |
||||
return nil, err |
||||
} |
||||
} else { |
||||
// Value optional, not error
|
||||
return nil, nil |
||||
} |
||||
} |
||||
|
||||
func getStringValue(obj map[string]interface{}, key string) (string, error) { |
||||
if untypedValue, ok := obj[key]; ok { |
||||
if value, ok := untypedValue.(string); ok { |
||||
return value, nil |
||||
} else { |
||||
err := fmt.Errorf("the field '%s' should be a string", key) |
||||
return "", err |
||||
} |
||||
} else { |
||||
err := fmt.Errorf("the field '%s' should be set", key) |
||||
return "", err |
||||
} |
||||
} |
Loading…
Reference in new issue