Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for workload identity #2619

Merged
merged 16 commits into from
May 15, 2024
12 changes: 12 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ jobs:
script: 'Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -AllowClobber -Force'
pwsh: 'true'
displayName: 'Install Powershell Az Module'
- task: AzureCLI@2
displayName: 'Set Up Workload Identity Environment Variables'
inputs:
azureSubscription: azcopyworkloadidentity
addSpnToEnvironment: true
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
echo "##vso[task.setvariable variable=AZURE_CLIENT_ID]$servicePrincipalId"
echo "##vso[task.setvariable variable=AZURE_FEDERATED_TOKEN]$idToken"
echo "##vso[task.setvariable variable=AZURE_TENANT_ID]$tenantId"
- task: GoTool@0
inputs:
version: $(AZCOPY_GOLANG_VERSION_COVERAGE)
Expand Down Expand Up @@ -228,6 +239,7 @@ jobs:
NEW_E2E_CLIENT_SECRET: $(AZCOPY_NEW_E2E_CLIENT_SECRET)
NEW_E2E_TENANT_ID: $(OAUTH_TENANT_ID)
NEW_E2E_AZCOPY_PATH: $(System.DefaultWorkingDirectory)/$(build_name)
NEW_E2E_ENVIRONMENT: "AzurePipeline"
displayName: 'E2E Test $(display_name) - AMD64'

- task: PublishBuildArtifacts@1
Expand Down
28 changes: 7 additions & 21 deletions cmd/credentialUtil.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,35 +104,21 @@ func GetOAuthTokenManagerInstance() (*common.UserOAuthTokenManager, error) {
}

// Fill up lca
lca.loginType = autoLoginType
switch autoLoginType {
case common.AutologinTypeSPN:
case common.EAutoLoginType.SPN().String():
lca.applicationID = glcm.GetEnvironmentVariable(common.EEnvironmentVariable.ApplicationID())
lca.certPath = glcm.GetEnvironmentVariable(common.EEnvironmentVariable.CertificatePath())
lca.certPass = glcm.GetEnvironmentVariable(common.EEnvironmentVariable.CertificatePassword())
lca.clientSecret = glcm.GetEnvironmentVariable(common.EEnvironmentVariable.ClientSecret())
lca.servicePrincipal = true

case common.AutologinTypeMSI:
case common.EAutoLoginType.MSI().String():
lca.identityClientID = glcm.GetEnvironmentVariable(common.EEnvironmentVariable.ManagedIdentityClientID())
lca.identityObjectID = glcm.GetEnvironmentVariable(common.EEnvironmentVariable.ManagedIdentityObjectID())
lca.identityResourceID = glcm.GetEnvironmentVariable(common.EEnvironmentVariable.ManagedIdentityResourceString())
lca.identity = true

case common.AutologinTypeDevice:
lca.identity = false

case common.AutologinTypeAzCLI:
lca.identity = false
lca.servicePrincipal = false
lca.psCred = false
lca.azCliCred = true

case common.AutologinTypePsCred:
lca.identity = false
lca.servicePrincipal = false
lca.azCliCred = false
lca.psCred = true

case common.EAutoLoginType.Device().String():
case common.EAutoLoginType.AzCLI().String():
case common.EAutoLoginType.PsCred().String():
case common.EAutoLoginType.Workload().String():
default:
glcm.Error("Invalid Auto-login type specified: " + autoLoginType)
return
Expand Down
102 changes: 38 additions & 64 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var lgCmd = &cobra.Command{
glcm.Info(environmentVariableNotice)
}

loginCmdArg.loginType = strings.ToLower(loginCmdArg.loginType)

err := loginCmdArg.process()
if err != nil {
// the errors from adal contains \r\n in the body, get rid of them to make the error easier to look at
Expand All @@ -65,16 +67,19 @@ func init() {

lgCmd.PersistentFlags().StringVar(&loginCmdArg.tenantID, "tenant-id", "", "The Azure Active Directory tenant ID to use for OAuth device interactive login.")
lgCmd.PersistentFlags().StringVar(&loginCmdArg.aadEndpoint, "aad-endpoint", "", "The Azure Active Directory endpoint to use. The default ("+common.DefaultActiveDirectoryEndpoint+") is correct for the public Azure cloud. Set this parameter when authenticating in a national cloud. Not needed for Managed Service Identity")
// Use identity which aligns to Azure powershell and CLI.
lgCmd.PersistentFlags().BoolVar(&loginCmdArg.identity, "identity", false, "Log in using virtual machine's identity, also known as managed service identity (MSI).")
// Use SPN certificate to log in.
lgCmd.PersistentFlags().BoolVar(&loginCmdArg.servicePrincipal, "service-principal", false, "Log in via Service Principal Name (SPN) by using a certificate or a secret. The client secret or certificate password must be placed in the appropriate environment variable. Type AzCopy env to see names and descriptions of environment variables.")
// Client ID of user-assigned identity.

lgCmd.PersistentFlags().BoolVar(&loginCmdArg.identity, "identity", false, "Deprecated. Please use --login-type=MSI. Log in using virtual machine's identity, also known as managed service identity (MSI).")
lgCmd.PersistentFlags().BoolVar(&loginCmdArg.servicePrincipal, "service-principal", false, "Deprecated. Please use --login-type=SPN. Log in via Service Principal Name (SPN) by using a certificate or a secret. The client secret or certificate password must be placed in the appropriate environment variable. Type AzCopy env to see names and descriptions of environment variables.")
// Deprecate these flags in favor of a new login type flag
_ = lgCmd.PersistentFlags().MarkHidden("identity")
_ = lgCmd.PersistentFlags().MarkHidden("service-principal")

lgCmd.PersistentFlags().StringVar(&loginCmdArg.loginType, "login-type", common.EAutoLoginType.Device().String(), "Default value is "+common.EAutoLoginType.Device().String()+". Specify the credential type to access Azure Resource, available values are "+strings.Join(common.ValidAutoLoginTypes(), ", ")+".")

// Managed Identity flags
lgCmd.PersistentFlags().StringVar(&loginCmdArg.identityClientID, "identity-client-id", "", "Client ID of user-assigned identity.")
// Resource ID of user-assigned identity.
lgCmd.PersistentFlags().StringVar(&loginCmdArg.identityResourceID, "identity-resource-id", "", "Resource ID of user-assigned identity.")

//login with SPN
// SPN flags
lgCmd.PersistentFlags().StringVar(&loginCmdArg.applicationID, "application-id", "", "Application ID of user-assigned identity. Required for service principal auth.")
lgCmd.PersistentFlags().StringVar(&loginCmdArg.certPath, "certificate-path", "", "Path to certificate for SPN authentication. Required for certificate-based service principal auth.")

Expand All @@ -91,8 +96,8 @@ type loginCmdArgs struct {

identity bool // Whether to use MSI.
servicePrincipal bool
azCliCred bool
psCred bool

loginType string

// Info of VM's user assigned identity, client or object ids of the service identity are required if
// your VM has multiple user-assigned managed identities.
Expand All @@ -109,75 +114,39 @@ type loginCmdArgs struct {
persistToken bool
}

func (lca loginCmdArgs) validate() error {
// Only support one kind of oauth login at same time.
switch {
case lca.identity:
if lca.servicePrincipal {
return errors.New("you can only log in with one type of auth at once")
}

// Consider only command-line parameters as env vars are a hassle to change and it's not like we'll use them here.
if lca.tenantID != "" || lca.applicationID != "" || lca.certPath != "" {
return errors.New("tenant ID/application ID/cert path/client secret cannot be used with identity")
}
case lca.servicePrincipal:
if lca.identity {
return errors.New("you can only log in with one type of auth at once")
}

if lca.identityClientID != "" || lca.identityObjectID != "" || lca.identityResourceID != "" {
return errors.New("identity client/object/resource ID are exclusive to managed service identity auth and are not compatible with service principal auth")
}

if lca.applicationID == "" || (lca.clientSecret == "" && lca.certPath == "") {
return errors.New("service principal auth requires an application ID, and client secret/certificate")
}
default: // OAuth login.
// This isn't necessary, but stands as a sanity check. It will never be hit.
if lca.servicePrincipal || lca.identity {
return errors.New("you can only log in with one type of auth at once")
}

// Consider only command-line parameters as env vars are a hassle to change and it's not like we'll use them here.
if lca.applicationID != "" || lca.certPath != "" {
return errors.New("application ID and certificate paths are exclusive to service principal auth and are not compatible with OAuth")
}

if lca.identityClientID != "" || lca.identityObjectID != "" || lca.identityResourceID != "" {
return errors.New("identity client/object/resource IDs are exclusive to managed service identity auth and are not compatible with OAuth")
}
}

return nil
}

func (lca loginCmdArgs) process() error {
// Validate login parameters.
if err := lca.validate(); err != nil {
return err
// Login type consolidation to allow backward compatibility.
if lca.servicePrincipal || lca.identity {
glcm.Warn("The flags --service-principal and --identity will be deprecated in a future release. Please use --login-type=SPN or --login-type=MSI instead.")
}
if lca.servicePrincipal {
lca.loginType = common.EAutoLoginType.SPN().String()
} else if lca.identity {
lca.loginType = common.EAutoLoginType.MSI().String()
} else if lca.servicePrincipal && lca.identity {
// This isn't necessary, but stands as a sanity check. It will never be hit.
return errors.New("you can only log in with one type of auth at once")
}
// Any required variables for login type will be validated by the Azure Identity SDK.
lca.loginType = strings.ToLower(lca.loginType)

uotm := GetUserOAuthTokenManagerInstance()
// Persist the token to cache, if login fulfilled successfully.

switch {
case lca.servicePrincipal:

switch lca.loginType {
case common.EAutoLoginType.SPN().String():
if lca.certPath != "" {
if err := uotm.CertLogin(lca.tenantID, lca.aadEndpoint, lca.certPath, lca.certPass, lca.applicationID, lca.persistToken); err != nil {
return err
}

glcm.Info("SPN Auth via cert succeeded.")
} else {
if err := uotm.SecretLogin(lca.tenantID, lca.aadEndpoint, lca.clientSecret, lca.applicationID, lca.persistToken); err != nil {
return err
}

glcm.Info("SPN Auth via secret succeeded.")
}
case lca.identity:
case common.EAutoLoginType.MSI().String():
if err := uotm.MSILogin(common.IdentityInfo{
ClientID: lca.identityClientID,
ObjectID: lca.identityObjectID,
Expand All @@ -187,16 +156,21 @@ func (lca loginCmdArgs) process() error {
}
// For MSI login, info success message to user.
glcm.Info("Login with identity succeeded.")
case lca.azCliCred:
case common.EAutoLoginType.AzCLI().String():
if err := uotm.AzCliLogin(lca.tenantID); err != nil {
return err
}
glcm.Info("Login with AzCliCreds succeeded")
case lca.psCred:
case common.EAutoLoginType.PsCred().String():
if err := uotm.PSContextToken(lca.tenantID); err != nil {
return err
}
glcm.Info("Login with Powershell context succeeded")
case common.EAutoLoginType.Workload().String():
if err := uotm.WorkloadIdentityLogin(lca.persistToken); err != nil {
return err
}
glcm.Info("Login with Workload Identity succeeded")
default:
if err := uotm.UserLogin(lca.tenantID, lca.aadEndpoint, lca.persistToken); err != nil {
return err
Expand Down
52 changes: 44 additions & 8 deletions common/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
package common

import (
"github.com/JeffreyRichter/enum/enum"
"reflect"
"runtime"
"strings"
)

type EnvironmentVariable struct {
Expand Down Expand Up @@ -82,18 +85,51 @@ func (EnvironmentVariable) UserDir() EnvironmentVariable {
}
}

const (
AutologinTypeSPN = "spn"
AutologinTypeMSI = "msi"
AutologinTypeDevice = "device"
AutologinTypeAzCLI = "azcli"
AutologinTypePsCred = "pscred"
)
var EAutoLoginType = AutoLoginType(0)

type AutoLoginType uint8

func (AutoLoginType) Device() AutoLoginType { return AutoLoginType(0) }
func (AutoLoginType) SPN() AutoLoginType { return AutoLoginType(1) }
func (AutoLoginType) MSI() AutoLoginType { return AutoLoginType(2) }
func (AutoLoginType) AzCLI() AutoLoginType { return AutoLoginType(3) }
func (AutoLoginType) PsCred() AutoLoginType { return AutoLoginType(4) }
func (AutoLoginType) Workload() AutoLoginType { return AutoLoginType(5) }
func (AutoLoginType) TokenStore() AutoLoginType { return AutoLoginType(255) } // Storage Explorer internal integration only. Do not add this to ValidAutoLoginTypes.

func (d AutoLoginType) String() string {
return strings.ToLower(enum.StringInt(d, reflect.TypeOf(d)))
}

func (d *AutoLoginType) Parse(s string) error {
// allow empty to mean "Enable"
if s == "" {
*d = EAutoLoginType.Device()
return nil
}

val, err := enum.ParseInt(reflect.TypeOf(d), s, true, true)
if err == nil {
*d = val.(AutoLoginType)
}
return err
}

func ValidAutoLoginTypes() []string {
return []string{
EAutoLoginType.Device().String() + " (Device code workflow)",
EAutoLoginType.SPN().String() + " (Service Principal)",
EAutoLoginType.MSI().String() + " (Managed Service Identity)",
EAutoLoginType.AzCLI().String() + " (Azure CLI)",
EAutoLoginType.PsCred().String() + " (Azure PowerShell)",
EAutoLoginType.Workload().String() + " (Workload Identity)",
}
}

func (EnvironmentVariable) AutoLoginType() EnvironmentVariable {
return EnvironmentVariable{
Name: "AZCOPY_AUTO_LOGIN_TYPE",
Description: "Specify the credential type to access Azure Resource without invoking the login command and using the OS secret store, available values SPN, MSI, DEVICE, AZCLI, and PSCRED - sequentially for Service Principal, Managed Service Identity, Device workflow, Azure CLI, or Azure PowerShell.",
Description: "Specify the credential type to access Azure Resource without invoking the login command and using the OS secret store, available values are " + strings.Join(ValidAutoLoginTypes(), ", ") + ".",
}
}

Expand Down
Loading
Loading