mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-22 16:36:27 +01:00
6d019a3c37
* Modify API version when running in Container App * Handle expires on from token response Response from IMDS does not always contain expires in value which is currently used to get the token expiry time. An example resources that doesn't provide it are Container Apps and App Service. Signed-off-by: Mattias Ängehov <mattias.angehov@castoredc.com> * Fix client id parameter for user assigned identity * Apply suggestions from code review --------- Signed-off-by: Mattias Ängehov <mattias.angehov@castoredc.com> Co-authored-by: Aliaksandr Valialkin <valyala@gmail.com>
306 lines
9.7 KiB
Go
306 lines
9.7 KiB
Go
package azure
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
|
)
|
|
|
|
var configMap = discoveryutils.NewConfigMap()
|
|
|
|
// Extract from the needed params from https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/azure/environments.go#L61
|
|
type cloudEnvironmentEndpoints struct {
|
|
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
|
|
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
|
|
}
|
|
|
|
// well-known azure cloud endpoints
|
|
// See https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/azure/environments.go#L34
|
|
var cloudEnvironments = map[string]*cloudEnvironmentEndpoints{
|
|
"AZURECHINACLOUD": {
|
|
ActiveDirectoryEndpoint: "https://login.chinacloudapi.cn",
|
|
ResourceManagerEndpoint: "https://management.chinacloudapi.cn",
|
|
},
|
|
"AZUREGERMANCLOUD": {
|
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.de",
|
|
ResourceManagerEndpoint: "https://management.microsoftazure.de",
|
|
},
|
|
"AZURECLOUD": {
|
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.com",
|
|
ResourceManagerEndpoint: "https://management.azure.com",
|
|
},
|
|
"AZUREPUBLICCLOUD": {
|
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.com",
|
|
ResourceManagerEndpoint: "https://management.azure.com",
|
|
},
|
|
"AZUREUSGOVERNMENT": {
|
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.us",
|
|
ResourceManagerEndpoint: "https://management.usgovcloudapi.net",
|
|
},
|
|
"AZUREUSGOVERNMENTCLOUD": {
|
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.us",
|
|
ResourceManagerEndpoint: "https://management.usgovcloudapi.net",
|
|
},
|
|
}
|
|
|
|
// apiConfig contains config for API server.
|
|
type apiConfig struct {
|
|
c *discoveryutils.Client
|
|
port int
|
|
resourceGroup string
|
|
subscriptionID string
|
|
tenantID string
|
|
|
|
refreshToken refreshTokenFunc
|
|
// tokenLock guards auth token and tokenExpireDeadline
|
|
tokenLock sync.Mutex
|
|
token string
|
|
tokenExpireDeadline time.Time
|
|
}
|
|
|
|
type refreshTokenFunc func() (string, time.Duration, error)
|
|
|
|
func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
|
|
v, err := configMap.Get(sdc, func() (interface{}, error) { return newAPIConfig(sdc, baseDir) })
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.(*apiConfig), nil
|
|
}
|
|
|
|
func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
|
|
if sdc.SubscriptionID == "" {
|
|
return nil, fmt.Errorf("missing `subscription_id` config option")
|
|
}
|
|
port := sdc.Port
|
|
if port == 0 {
|
|
port = 80
|
|
}
|
|
|
|
ac, err := sdc.HTTPClientConfig.NewConfig(baseDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse auth config: %w", err)
|
|
}
|
|
proxyAC, err := sdc.ProxyClientConfig.NewConfig(baseDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse proxy auth config: %w", err)
|
|
}
|
|
|
|
environment := sdc.Environment
|
|
if environment == "" {
|
|
environment = "AZURECLOUD"
|
|
}
|
|
env, err := getCloudEnvByName(environment)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot read configs for `environment: %q`: %w", environment, err)
|
|
}
|
|
|
|
refreshToken, err := getRefreshTokenFunc(sdc, ac, proxyAC, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c, err := discoveryutils.NewClient(env.ResourceManagerEndpoint, ac, sdc.ProxyURL, proxyAC)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot create client for %q: %w", env.ResourceManagerEndpoint, err)
|
|
}
|
|
cfg := &apiConfig{
|
|
c: c,
|
|
port: port,
|
|
resourceGroup: sdc.ResourceGroup,
|
|
subscriptionID: sdc.SubscriptionID,
|
|
tenantID: sdc.TenantID,
|
|
|
|
refreshToken: refreshToken,
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func getCloudEnvByName(name string) (*cloudEnvironmentEndpoints, error) {
|
|
name = strings.ToUpper(name)
|
|
// Special case, azure cloud k8s cluster, read content from file.
|
|
// See https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/azure/environments.go#L301
|
|
if name == "AZURESTACKCLOUD" {
|
|
return readCloudEndpointsFromFile(os.Getenv("AZURE_ENVIRONMENT_FILEPATH"))
|
|
}
|
|
env := cloudEnvironments[name]
|
|
if env == nil {
|
|
var supportedEnvs []string
|
|
for envName := range cloudEnvironments {
|
|
supportedEnvs = append(supportedEnvs, envName)
|
|
}
|
|
return nil, fmt.Errorf("unsupported `environment: %q`; supported values: %s", name, strings.Join(supportedEnvs, ","))
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
func readCloudEndpointsFromFile(filePath string) (*cloudEnvironmentEndpoints, error) {
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot file %q: %w", filePath, err)
|
|
}
|
|
var cee cloudEnvironmentEndpoints
|
|
if err := json.Unmarshal(data, &cee); err != nil {
|
|
return nil, fmt.Errorf("cannot parse cloud environment endpoints from file %q: %w", filePath, err)
|
|
}
|
|
return &cee, nil
|
|
}
|
|
|
|
func getRefreshTokenFunc(sdc *SDConfig, ac, proxyAC *promauth.Config, env *cloudEnvironmentEndpoints) (refreshTokenFunc, error) {
|
|
var tokenEndpoint, tokenAPIPath string
|
|
var modifyRequest func(request *http.Request)
|
|
authenticationMethod := sdc.AuthenticationMethod
|
|
if authenticationMethod == "" {
|
|
authenticationMethod = "OAuth"
|
|
}
|
|
switch strings.ToLower(authenticationMethod) {
|
|
case "oauth":
|
|
if sdc.TenantID == "" {
|
|
return nil, fmt.Errorf("missing `tenant_id` config option for `authentication_method: Oauth`")
|
|
}
|
|
if sdc.ClientID == "" {
|
|
return nil, fmt.Errorf("missing `client_id` config option for `authentication_method: OAuth`")
|
|
}
|
|
if sdc.ClientSecret.String() == "" {
|
|
return nil, fmt.Errorf("missing `client_secrect` config option for `authentication_method: OAuth`")
|
|
}
|
|
q := url.Values{
|
|
"grant_type": []string{"client_credentials"},
|
|
"client_id": []string{sdc.ClientID},
|
|
"client_secret": []string{sdc.ClientSecret.String()},
|
|
"resource": []string{env.ResourceManagerEndpoint},
|
|
}
|
|
authParams := q.Encode()
|
|
tokenAPIPath = "/" + sdc.TenantID + "/oauth2/token"
|
|
tokenEndpoint = env.ActiveDirectoryEndpoint
|
|
modifyRequest = func(request *http.Request) {
|
|
request.Body = io.NopCloser(strings.NewReader(authParams))
|
|
request.Method = http.MethodPost
|
|
}
|
|
case "managedidentity":
|
|
endpoint := "http://169.254.169.254/metadata/identity/oauth2/token"
|
|
if ep := os.Getenv("MSI_ENDPOINT"); ep != "" {
|
|
endpoint = ep
|
|
}
|
|
endpointURL, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse MSI endpoint url %q: %w", endpoint, err)
|
|
}
|
|
q := endpointURL.Query()
|
|
|
|
msiSecret := os.Getenv("MSI_SECRET")
|
|
identityHeader := os.Getenv("IDENTITY_HEADER")
|
|
clientIDParam := "client_id"
|
|
apiVersion := "2018-02-01"
|
|
if msiSecret != "" {
|
|
clientIDParam = "clientid"
|
|
apiVersion = "2017-09-01"
|
|
}
|
|
if identityHeader != "" {
|
|
clientIDParam = "client_id"
|
|
apiVersion = "2019-08-01"
|
|
}
|
|
q.Set("api-version", apiVersion)
|
|
q.Set(clientIDParam, sdc.ClientID)
|
|
q.Set("resource", env.ResourceManagerEndpoint)
|
|
endpointURL.RawQuery = q.Encode()
|
|
tokenAPIPath = endpointURL.RequestURI()
|
|
tokenEndpoint = endpointURL.Scheme + "://" + endpointURL.Host
|
|
modifyRequest = func(request *http.Request) {
|
|
if msiSecret != "" {
|
|
request.Header.Set("secret", msiSecret)
|
|
if identityHeader != "" {
|
|
request.Header.Set("X-IDENTITY-HEADER", msiSecret)
|
|
}
|
|
} else {
|
|
request.Header.Set("Metadata", "true")
|
|
}
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported `authentication_method: %q` only `OAuth` and `ManagedIdentity` are supported", authenticationMethod)
|
|
}
|
|
|
|
authClient, err := discoveryutils.NewClient(tokenEndpoint, ac, sdc.ProxyURL, proxyAC)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot build auth client: %w", err)
|
|
}
|
|
refreshToken := func() (string, time.Duration, error) {
|
|
data, err := authClient.GetAPIResponseWithReqParams(tokenAPIPath, modifyRequest)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
var tr tokenResponse
|
|
if err := json.Unmarshal(data, &tr); err != nil {
|
|
return "", 0, fmt.Errorf("cannot parse token auth response %q: %w", data, err)
|
|
}
|
|
|
|
expiresInSeconds, err := parseTokenExpiry(tr)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
return tr.AccessToken, time.Second * time.Duration(expiresInSeconds), nil
|
|
}
|
|
return refreshToken, nil
|
|
}
|
|
|
|
// parseTokenExpiry returns token expiry in seconds
|
|
func parseTokenExpiry(tr tokenResponse) (int64, error) {
|
|
var expiresInSeconds int64
|
|
var err error
|
|
|
|
if tr.ExpiresIn == "" {
|
|
var expiresOnSeconds int64
|
|
expiresOnSeconds, err = strconv.ParseInt(tr.ExpiresOn, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cannot parse expiresOn=%q in auth token response: %w", tr.ExpiresOn, err)
|
|
}
|
|
expiresInSeconds = expiresOnSeconds - time.Now().Unix()
|
|
} else {
|
|
expiresInSeconds, err = strconv.ParseInt(tr.ExpiresIn, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cannot parse expiresIn=%q auth token response: %w", tr.ExpiresIn, err)
|
|
}
|
|
}
|
|
|
|
return expiresInSeconds, nil
|
|
}
|
|
|
|
// mustGetAuthToken returns auth token
|
|
// in case of error, logs error and return empty token
|
|
func (ac *apiConfig) mustGetAuthToken() string {
|
|
ac.tokenLock.Lock()
|
|
defer ac.tokenLock.Unlock()
|
|
|
|
ct := time.Now()
|
|
if ac.tokenExpireDeadline.Sub(ct) > time.Second*30 {
|
|
return ac.token
|
|
}
|
|
token, expiresDuration, err := ac.refreshToken()
|
|
if err != nil {
|
|
logger.Errorf("cannot refresh azure auth token: %s", err)
|
|
return ""
|
|
}
|
|
ac.token = token
|
|
ac.tokenExpireDeadline = ct.Add(expiresDuration)
|
|
return ac.token
|
|
}
|
|
|
|
// tokenResponse represent response from oauth2 azure token service
|
|
//
|
|
// https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-go
|
|
type tokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
ExpiresIn string `json:"expires_in"`
|
|
ExpiresOn string `json:"expires_on"`
|
|
}
|