Skip to content

Instantly share code, notes, and snippets.

@sebnyberg
Last active February 21, 2023 16:44
Show Gist options
  • Save sebnyberg/19d02d5207f82e4076e9266af6783211 to your computer and use it in GitHub Desktop.
Save sebnyberg/19d02d5207f82e4076e9266af6783211 to your computer and use it in GitHub Desktop.
Some Azure Go SDK auth things
package azurex
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/resources/mgmt/subscriptions"
mgmtstorage "github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/storage/mgmt/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
)
// getAccountClient retrieves a blob storage account client in an automated way
// based on the local Azure CLI session or SP credentials in environment vars.
//
// This function will pick the first subscription available to the user, and
// the first account with the requested name within the subscription.
//
func getAccountClient(
ctx context.Context,
accountName string,
) (storage.BlobStorageClient, error) {
nilClient := storage.BlobStorageClient{}
// Initialize authorizer based either on CLI or environment variables.
authorizer, err := auth.NewAuthorizerFromCLIWithResource(
azure.PublicCloud.ResourceManagerEndpoint,
)
if err != nil { // Try environment
cfg := &auth.ClientCredentialsConfig{
ClientID: os.Getenv("AZURE_CLIENT_ID"),
ClientSecret: os.Getenv("AZURE_CLIENT_SECRET"),
TenantID: os.Getenv("AZURE_TENANT_ID"),
Resource: azure.PublicCloud.ResourceManagerEndpoint,
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
}
authorizer, err = cfg.Authorizer()
if err != nil {
return nilClient, fmt.Errorf(
"auth initialization failed, %v", err)
}
}
// Get subscription ID.
subID, err := inferSubscriptionIDFromUser(ctx, authorizer)
if err != nil {
return nilClient, fmt.Errorf(
"could not infer subscription id for logged in user, %v", err)
}
// Find the resource group name of the account
rg, err := findAccountRG(ctx, authorizer, subID, accountName)
if err != nil {
return nilClient, fmt.Errorf("could not find storage account, %v", err)
}
// Get account key
accountKey, err := getAccountKey(ctx, authorizer, subID, rg, accountName)
if err != nil {
return nilClient, err
}
// Initialize and return client
client, err := storage.NewBasicClient(accountName, accountKey)
if err != nil {
return nilClient, fmt.Errorf("client, %w", err)
}
return client.GetBlobService(), nil
}
// getAccountKey lists account keys and picks the first one from the list.
func getAccountKey(
ctx context.Context,
authorizer autorest.Authorizer,
subscriptionID string,
resourceGroupName string,
accountName string,
) (string, error) {
accountsClient := mgmtstorage.NewAccountsClient(subscriptionID)
accountsClient.Authorizer = authorizer
listKeysResult, err := accountsClient.ListKeys(ctx,
resourceGroupName, accountName)
if err != nil {
return "", err
}
if len(*listKeysResult.Keys) == 0 {
return "", errors.New("failed to list keys in storage acc")
}
key := *(*listKeysResult.Keys)[0].Value
return key, nil
}
// findAccountRG lists storage accounts available within the subscription and
// returns the resourge group name of the first matching storage account, or an
// error if the storage account could not be found.
func findAccountRG(
ctx context.Context,
authorizer autorest.Authorizer,
subscriptionID string,
accountName string,
) (string, error) {
accountsClient := mgmtstorage.NewAccountsClient(subscriptionID)
accountsClient.Authorizer = authorizer
result, err := accountsClient.List(ctx)
if err != nil {
return "", fmt.Errorf("list blob accounts err, %w", err)
}
for _, val := range *result.Value {
if *val.Name == accountName {
idparts := strings.Split(*val.ID, "/")
if len(idparts) < 4 {
return "", errors.New("invalid account id")
}
return idparts[4], nil
}
}
return "", errors.New("not found")
}
// inferSubscriptionIDFromUser lists subscriptions available to the user that is
// embedded in the authorizer, and returns the first available subscription.
func inferSubscriptionIDFromUser(
ctx context.Context,
authorizer autorest.Authorizer,
) (string, error) {
subscriptionClient := subscriptions.NewClient()
subscriptionClient.Authorizer = authorizer
res, err := subscriptionClient.List(ctx)
if err != nil {
return "", fmt.Errorf("failed to list subscriptions, %v", err)
}
subs := res.Values()
if len(subs) < 1 {
return "", errors.New("logged in user does not have access to any subscriptions")
}
for _, sub := range subs {
return *sub.SubscriptionID, nil
}
return "", errors.New("subscription id not found")
}
import (
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
)
func getMasterKey(ctx context.Context, appName string) (string, error) {
authorizer, err := auth.NewAuthorizerFromCLI()
if err != nil {
authorizer, err = auth.NewAuthorizerFromEnvironment()
if err != nil {
return "", fmt.Errorf(
"auth initialization failed, %v", err)
}
}
subscriptionID, err := findSubscriptionID(ctx, authorizer)
if err != nil {
return "", fmt.Errorf(
"failed to infer subscription ID based on logged in user, %v", err)
}
site, err := findSite(ctx, authorizer, subscriptionID, appName)
if err != nil {
return "", fmt.Errorf("could not find site with name %v, %v", appName, err)
}
appsClient := web.NewAppsClient(subscriptionID)
appsClient.Authorizer = authorizer
keys, err := appsClient.ListHostKeys(ctx, *site.ResourceGroup, *site.Name)
if err != nil {
return "", fmt.Errorf(
"failed to list host keys for app with name %v, %v", appName, err)
}
return *keys.MasterKey, nil
}
func findSubscriptionID(
ctx context.Context,
authorizer autorest.Authorizer,
) (string, error) {
subscriptionClient := subscriptions.NewClient()
subscriptionClient.Authorizer = authorizer
res, err := subscriptionClient.List(ctx)
if err != nil {
return "", fmt.Errorf("failed to list subscriptions, %v", err)
}
subs := res.Values()
if len(subs) < 1 {
return "", errors.New("logged in user does not have access to any subscriptions")
}
for _, sub := range subs {
return *sub.SubscriptionID, nil
}
return "", errors.New("subscription id not found")
}
func findSite(
ctx context.Context,
authorizer autorest.Authorizer,
subscriptionID string,
name string,
) (*web.Site, error) {
appsClient := web.NewAppsClient(subscriptionID)
appsClient.Authorizer = authorizer
// Find app service that we are looking for
apps, err := appsClient.List(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list web apps, %v", err)
}
for {
for _, site := range apps.Values() {
if *site.Name == name {
return &site, nil
}
}
if !apps.NotDone() {
break
}
if err := apps.NextWithContext(ctx); err != nil {
return nil, fmt.Errorf("failed fetch next page of web apps, %v", err)
}
}
return nil, errors.New("site not found")
}
func findResourceGroup(
ctx context.Context,
accountName string,
subscriptionID string,
token string,
) (string, error) {
var jsonResp struct {
Value []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"value"`
}
url := fmt.Sprintf("https://management.azure.com/subscriptions/%v/providers/Microsoft.Storage/storageAccounts?api-version=2021-04-01",
subscriptionID,
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
if err := json.NewDecoder(resp.Body).Decode(&jsonResp); err != nil {
return "", err
}
for _, val := range jsonResp.Value {
if val.Name == accountName {
parts := strings.Split(val.ID, "/")
if len(parts) < 4 {
return "", fmt.Errorf("failed to parse account resource name")
}
return parts[3], nil
}
}
return "", errors.New("coult not find storage account")
}
func findSubscriptionID(
ctx context.Context,
token string,
) (string, error) {
var jsonResp struct {
Value []struct {
SubscriptionID string `json:"subscriptionId"`
} `json:"value"`
}
url := "https://management.azure.com/subscriptions?api-version=2020-01-01"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
if err := json.NewDecoder(resp.Body).Decode(&jsonResp); err != nil {
return "", err
}
if len(jsonResp.Value) == 0 {
return "", errors.New("no subscriptions available for service principal")
}
return jsonResp.Value[0].SubscriptionID, nil
}
func maybeGetTokenFromCLI() (string, error) {
token, err := cli.GetTokenFromCLI(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return "", err
}
adalToken, err := token.ToADALToken()
if err != nil {
return "", err
}
return adalToken.AccessToken, nil
}
func maybeGetTokenFromEnv() (string, error) {
for _, envName := range []string{
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"AZURE_TENANT_ID",
} {
if os.Getenv(envName) == "" {
return "", fmt.Errorf("azure token from env failed, %v is unset", envName)
}
}
cfg := &auth.ClientCredentialsConfig{
ClientID: os.Getenv("AZURE_CLIENT_ID"),
ClientSecret: os.Getenv("AZURE_CLIENT_SECRET"),
TenantID: os.Getenv("AZURE_TENANT_ID"),
Resource: azure.PublicCloud.ResourceManagerEndpoint,
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
}
token, err := cfg.ServicePrincipalToken()
if err != nil {
return "", err
}
if err := token.EnsureFresh(); err != nil {
return "", err
}
return token.OAuthToken(), nil
}
@sebnyberg
Copy link
Author

Assumptions:

  • Logged in user's first subscription ID is the one where the function app resides
  • The function app has a unique name within the subscription

Process:

  • Initialize auth session based on CLI, falling back to environment if CLI is not successful
  • List subscription (IDs) available for the logged in user, use the first one
  • List Web Apps, fetch the first app which matches the app name
  • Use Web App properties to list host keys
  • Return the master key

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment