245 lines
6.7 KiB
Go
245 lines
6.7 KiB
Go
package util
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"time"
|
|
|
|
credentials "cloud.google.com/go/iam/credentials/apiv1"
|
|
"cloud.google.com/go/iam/credentials/apiv1/credentialspb"
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/go-resty/resty/v2"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
func GetKubernetesServiceAccountToken(serviceAccountTokenPath string) (string, error) {
|
|
|
|
if serviceAccountTokenPath == "" {
|
|
serviceAccountTokenPath = DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH
|
|
}
|
|
|
|
token, err := os.ReadFile(serviceAccountTokenPath)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(token), nil
|
|
|
|
}
|
|
|
|
func buildAzureMetadataServiceURL(resource string, clientID string) string {
|
|
azureURL := AZURE_METADATA_SERVICE_URL + AZURE_DEFAULT_RESOURCE
|
|
if resource != "" {
|
|
azureURL = AZURE_METADATA_SERVICE_URL + url.QueryEscape(resource)
|
|
}
|
|
if clientID != "" {
|
|
azureURL += "&client_id=" + url.QueryEscape(clientID)
|
|
}
|
|
return azureURL
|
|
}
|
|
|
|
// GetAzureMetadataToken fetches a JWT from the Azure IMDS endpoint.
|
|
// The optional clientID parameter targets a specific User-Assigned Managed Identity;
|
|
// pass "" for System-Assigned Managed Identity.
|
|
func GetAzureMetadataToken(httpClient *resty.Client, customResource string, clientID string) (string, error) {
|
|
|
|
type AzureMetadataResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
}
|
|
|
|
metadataResponse := AzureMetadataResponse{}
|
|
|
|
response, err := httpClient.R().
|
|
SetResult(&metadataResponse).
|
|
SetHeader("Metadata", "true").
|
|
SetHeader("Accept", "application/json").
|
|
Get(buildAzureMetadataServiceURL(customResource, clientID))
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if response.IsError() {
|
|
return "", fmt.Errorf("GetAzureMetadataToken: Unsuccessful response [%v %v] [status-code=%v] [Error: %s]", response.Request.Method, response.Request.URL, response.StatusCode(), TryParseErrorBody(response))
|
|
}
|
|
|
|
return metadataResponse.AccessToken, nil
|
|
}
|
|
|
|
func GetGCPMetadataToken(httpClient *resty.Client, identityID string) (string, error) {
|
|
|
|
res, err := httpClient.R().
|
|
SetHeader("Metadata-Flavor", "Google").
|
|
Get(fmt.Sprintf("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s&format=full", identityID))
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if res.IsError() {
|
|
return "", fmt.Errorf("GetGCPMetadataToken: Unsuccessful response [%v %v] [status-code=%v] [Error: %s]", res.Request.Method, res.Request.URL, res.StatusCode(), TryParseErrorBody(res))
|
|
}
|
|
|
|
return res.String(), nil
|
|
|
|
}
|
|
|
|
func GetAwsEC2IdentityDocumentRegion(timeout int) (string, error) {
|
|
|
|
type AwsIdentityDocument struct {
|
|
Region string `json:"region"`
|
|
}
|
|
|
|
httpClient := resty.New().SetTimeout(time.Duration(timeout) * time.Millisecond)
|
|
|
|
res, err := httpClient.R().
|
|
SetHeader("X-aws-ec2-metadata-token-ttl-seconds", "21600").
|
|
Put(AWS_EC2_METADATA_TOKEN_URL)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if res.IsError() {
|
|
return "", fmt.Errorf("GetAwsEC2IdentityDocumentRegion: Unsuccessful response [%v %v] [status-code=%v] [Error: %s]", res.Request.Method, res.Request.URL, res.StatusCode(), TryParseErrorBody(res))
|
|
}
|
|
|
|
metadataToken := res.String()
|
|
|
|
res, err = httpClient.R().
|
|
SetHeader("X-aws-ec2-metadata-token", metadataToken).
|
|
SetHeader("Accept", "application/json").
|
|
Get(AWS_EC2_INSTANCE_IDENTITY_DOCUMENT_URL)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if res.IsError() {
|
|
return "", fmt.Errorf("GetAwsEC2IdentityDocumentRegion: Unsuccessful response [%v %v] [status-code=%v] [Error: %s]", res.Request.Method, res.Request.URL, res.StatusCode(), TryParseErrorBody(res))
|
|
}
|
|
|
|
// For some reason using .SetResult(&AwsIdentityDocument{}) doesn't work and just results in an empty object. This works though..
|
|
var identityDocument AwsIdentityDocument
|
|
err = json.Unmarshal(res.Body(), &identityDocument)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return identityDocument.Region, nil
|
|
|
|
}
|
|
|
|
func GetGCPIamServiceAccountToken(identityID string, serviceAccountKeyPath string) (string, error) {
|
|
|
|
type JwtPayload struct {
|
|
Sub string `json:"sub"`
|
|
Aud string `json:"aud"`
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
serviceAccountKey, err := os.ReadFile(serviceAccountKeyPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var creds map[string]string
|
|
if err := json.Unmarshal(serviceAccountKey, &creds); err != nil {
|
|
return "", fmt.Errorf("failed to unmarshal service account key: %v", err)
|
|
}
|
|
|
|
clientEmail := creds["client_email"]
|
|
if clientEmail == "" {
|
|
return "", fmt.Errorf("client email not found in service account key")
|
|
}
|
|
|
|
payload := JwtPayload{
|
|
Sub: clientEmail,
|
|
Aud: identityID,
|
|
}
|
|
|
|
payloadJSON, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal JWT payload: %v", err)
|
|
}
|
|
|
|
iamCredentialsClient, err := credentials.NewIamCredentialsClient(ctx, option.WithCredentialsFile(serviceAccountKeyPath)) //nolint:staticcheck // deprecated but no drop-in replacement available yet
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create IAM credentials client: %v", err)
|
|
}
|
|
|
|
defer iamCredentialsClient.Close() //nolint:errcheck
|
|
|
|
signJwtRequest := &credentialspb.SignJwtRequest{
|
|
Name: fmt.Sprintf("projects/-/serviceAccounts/%s", clientEmail),
|
|
Payload: string(payloadJSON),
|
|
}
|
|
|
|
resp, err := iamCredentialsClient.SignJwt(ctx, signJwtRequest)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to sign JWT: %v. Ensure the IAM Service Account Credentials API is enabled", err)
|
|
}
|
|
|
|
signedJwt := resp.SignedJwt
|
|
if signedJwt == "" {
|
|
return "", fmt.Errorf("failed to sign JWT: signedJwt is empty")
|
|
}
|
|
|
|
return signedJwt, nil
|
|
|
|
}
|
|
|
|
func GetAwsRegion() (string, error) {
|
|
// in Lambda environments, the region is available in the AWS_REGION environment variable
|
|
region := os.Getenv("AWS_REGION")
|
|
|
|
if region != "" {
|
|
return region, nil
|
|
}
|
|
|
|
// in EC2 environments, the region is available in the identity doc
|
|
|
|
region, err := GetAwsEC2IdentityDocumentRegion(5000)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return region, nil
|
|
|
|
}
|
|
|
|
func RetrieveAwsCredentials() (credentials aws.Credentials, region string, err error) {
|
|
presetAwsCfg, err := config.LoadDefaultConfig(context.TODO())
|
|
|
|
if err == nil && presetAwsCfg.Region != "" {
|
|
creds, err := presetAwsCfg.Credentials.Retrieve(context.TODO())
|
|
if err == nil {
|
|
return creds, presetAwsCfg.Region, nil
|
|
}
|
|
}
|
|
|
|
awsRegion, err := GetAwsRegion()
|
|
if err != nil {
|
|
return aws.Credentials{}, "", err
|
|
}
|
|
|
|
awsCfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(awsRegion))
|
|
if err != nil {
|
|
return aws.Credentials{}, "", fmt.Errorf("unable to load SDK config, %v", err)
|
|
}
|
|
|
|
creds, err := awsCfg.Credentials.Retrieve(context.TODO())
|
|
if err != nil {
|
|
return aws.Credentials{}, "", fmt.Errorf("error retrieving credentials: %v", err)
|
|
}
|
|
|
|
return creds, awsRegion, nil
|
|
}
|