open-consul/agent/connect/ca/provider_vault_auth_azure.go
John Eikenberry 5ac637f07d
add provider ca auth-method support for azure
Does the required dance with the local HTTP endpoint to get the required
data for the jwt based auth setup in Azure. Keeps support for 'legacy'
mode where all login data is passed on via the auth methods parameters.
Refactored check for hardcoded /login fields.
2023-03-01 00:07:33 +00:00

143 lines
3.7 KiB
Go

package ca
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
)
func NewAzureAuthClient(authMethod *structs.VaultAuthMethod) (*VaultAuthClient, error) {
params := authMethod.Params
authClient := NewVaultAPIAuthClient(authMethod, "")
// check for login data already in params (for backwards compability)
legacyKeys := []string{
"vm_name", "vmss_name", "resource_group_name", "subscription_id", "jwt",
}
if legacyCheck(params, legacyKeys...) {
return authClient, nil
}
role, ok := params["role"].(string)
if !ok || strings.TrimSpace(role) == "" {
return nil, fmt.Errorf("missing 'role' value")
}
resource, ok := params["resource"].(string)
if !ok || strings.TrimSpace(resource) == "" {
return nil, fmt.Errorf("missing 'resource' value")
}
authClient.LoginDataGen = AzureLoginDataGen
return authClient, nil
}
var ( // use variables so we can change these in tests
instanceEndpoint = "http://169.254.169.254/metadata/instance"
identityEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
// minimum version 2018-02-01 needed for identity metadata
apiVersion = "2018-02-01"
)
type instanceData struct {
Compute Compute
}
type Compute struct {
Name string
ResourceGroupName string
SubscriptionID string
VMScaleSetName string
}
type identityData struct {
AccessToken string `json:"access_token"`
}
func AzureLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error) {
params := authMethod.Params
role := params["role"].(string)
metaConf := map[string]string{
"role": role,
"resource": params["resource"].(string),
}
if objectID, ok := params["object_id"].(string); ok {
metaConf["object_id"] = objectID
}
if clientID, ok := params["client_id"].(string); ok {
metaConf["client_id"] = clientID
}
// Fetch instance data
var instance instanceData
body, err := getMetadataInfo(instanceEndpoint, nil)
if err != nil {
return nil, err
}
err = jsonutil.DecodeJSON(body, &instance)
if err != nil {
return nil, fmt.Errorf("error parsing instance metadata response: %w", err)
}
// Fetch JWT
var identity identityData
body, err = getMetadataInfo(identityEndpoint, metaConf)
if err != nil {
return nil, err
}
err = jsonutil.DecodeJSON(body, &identity)
if err != nil {
return nil, fmt.Errorf("error parsing instance metadata response: %w", err)
}
data := map[string]interface{}{
"role": role,
"vm_name": instance.Compute.Name,
"vmss_name": instance.Compute.VMScaleSetName,
"resource_group_name": instance.Compute.ResourceGroupName,
"subscription_id": instance.Compute.SubscriptionID,
"jwt": identity.AccessToken,
}
return data, nil
}
func getMetadataInfo(endpoint string, query map[string]string) ([]byte, error) {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("api-version", apiVersion)
for k, v := range query {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
req.Header.Set("Metadata", "true")
req.Header.Set("User-Agent", "Consul")
client := cleanhttp.DefaultClient()
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error fetching metadata from %s: %w", endpoint, err)
}
if resp == nil {
return nil, fmt.Errorf("empty response fetching metadata from %s", endpoint)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading metadata from %s: %w", endpoint, err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error response in metadata from %s: %s", endpoint, body)
}
return body, nil
}