Skip to content

Commit

Permalink
PS and CLI creds (#2433)
Browse files Browse the repository at this point in the history
* AzCLI Creds

* Powershell and CLI Creds

* Consider unix timestamps for PS

* Testcase

* Fix lint error

* Testcase

* Install Powershell Az module on Windows

* FIx pipeline error

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Force powershell output to a format

* Test

* Test2

* Fix output format
  • Loading branch information
nakulkar-msft authored Dec 1, 2023
1 parent ef27247 commit 26640e7
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 25 deletions.
7 changes: 7 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ jobs:
imageName: 'windows-2019'
build_name: 'azcopy_windows_amd64.exe'
display_name: "Windows"
type: 'windows'
go_path: 'C:\Users\VssAdministrator\go\bin\'
suffix: '.exe'
run_e2e: 'go test -timeout=1h -v ./e2etest > test.txt'
Expand All @@ -152,6 +153,12 @@ jobs:
vmImage: $(imageName)

steps:
- task: PowerShell@2
condition: eq(variables.type, 'windows' )
inputs:
targetType: 'inline'
script: 'Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -AllowClobber -Force'
displayName: 'Install Powershell Az Module'
- task: GoTool@0
inputs:
version: $(AZCOPY_GOLANG_VERSION_COVERAGE)
Expand Down
14 changes: 13 additions & 1 deletion cmd/credentialUtil.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func GetOAuthTokenManagerInstance() (*common.UserOAuthTokenManager, error) {
return
}

if autoLoginType != "SPN" && autoLoginType != "MSI" && autoLoginType != "DEVICE" {
if autoLoginType != "SPN" && autoLoginType != "MSI" && autoLoginType != "DEVICE" && autoLoginType != "AZCLI" && autoLoginType != "PSCRED" {
glcm.Error("Invalid Auto-login type specified.")
return
}
Expand Down Expand Up @@ -118,6 +118,18 @@ func GetOAuthTokenManagerInstance() (*common.UserOAuthTokenManager, error) {

case "DEVICE":
lca.identity = false

case "AZCLI":
lca.identity = false
lca.servicePrincipal = false
lca.psCred = false
lca.azCliCred = true

case "PSCRED":
lca.identity = false
lca.servicePrincipal = false
lca.azCliCred = false
lca.psCred = true
}

lca.persistToken = false
Expand Down
16 changes: 14 additions & 2 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
var loginCmdArg = loginCmdArgs{tenantID: common.DefaultTenantID}

var loginNotice = "'azcopy %s' command will be deprecated starting release 10.22. " +
"Use auto-login instead. Visit %s to know more."
"Use auto-login instead. Visit %s to know more."
var autoLoginURL = "https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-authorize-azure-active-directory#authorize-without-a-secret-store "

var lgCmd = &cobra.Command{
Expand Down Expand Up @@ -85,7 +85,7 @@ func init() {
lgCmd.PersistentFlags().StringVar(&loginCmdArg.certPath, "certificate-path", "", "Path to certificate for SPN authentication. Required for certificate-based service principal auth.")

// Deprecate the identity-object-id flag
_ = lgCmd.PersistentFlags().MarkHidden("identity-object-id") // Object ID of user-assigned identity.
_ = lgCmd.PersistentFlags().MarkHidden("identity-object-id") // Object ID of user-assigned identity.
lgCmd.PersistentFlags().StringVar(&loginCmdArg.identityObjectID, "identity-object-id", "", "Object ID of user-assigned identity. This parameter is deprecated. Please use client id or resource id")

}
Expand All @@ -97,6 +97,8 @@ type loginCmdArgs struct {

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

// 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 Down Expand Up @@ -191,6 +193,16 @@ func (lca loginCmdArgs) process() error {
}
// For MSI login, info success message to user.
glcm.Info("Login with identity succeeded.")
case lca.azCliCred:
if err := uotm.AzCliLogin(lca.tenantID); err != nil {
return err
}
glcm.Info("Login with AzCliCreds succeeded")
case lca.psCred:
if err := uotm.PSContextToken(lca.tenantID); err != nil {
return err
}
glcm.Info("Login with Powershell context succeeded")
default:
if err := uotm.UserLogin(lca.tenantID, lca.aadEndpoint, lca.persistToken); err != nil {
return err
Expand Down
172 changes: 172 additions & 0 deletions common/azure_ps_context_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//go:build go1.18
// +build go1.18

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package common

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"sync"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)

const credNamePSContext = "PSContextCredential"

type PSTokenProvider func(ctx context.Context, resource string, tenant string) ([]byte, error)
func validTenantID(tenantID string) bool {
match, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", tenantID)
if err != nil {
return false
}
return match
}

func resolveTenant(defaultTenant, specified, credName string, additionalTenants []string) (string, error) {
if specified == "" || specified == defaultTenant {
return defaultTenant, nil
}
if defaultTenant == "adfs" {
return "", errors.New("ADFS doesn't support tenants")
}
if !validTenantID(specified) {
return "", errors.New("Invalid tenant")
}
for _, t := range additionalTenants {
if t == "*" || t == specified {
return specified, nil
}
}
return "", fmt.Errorf(`%s isn't configured to acquire tokens for tenant %q. To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add "*" to allow acquiring tokens for any tenant`, credName, specified)
}
// PowershellContextCredentialOptions contains optional parameters for AzureDeveloperCLICredential.
type PowershellContextCredentialOptions struct {
// TenantID identifies the tenant the credential should authenticate in. Defaults to the azd environment,
// which is the tenant of the selected Azure subscription.
TenantID string

tokenProvider PSTokenProvider
}

// PowershellContextCredential authenticates as the identity logged in to the [Azure Developer CLI].
//
// [Azure Developer CLI]: https://learn.microsoft.com/azure/developer/azure-developer-cli/overview
type PowershellContextCredential struct {
mu *sync.Mutex
opts PowershellContextCredentialOptions
}

// NewPowershellContextCredential constructs an AzureDeveloperCLICredential. Pass nil to accept default options.
func NewPowershellContextCredential(options *PowershellContextCredentialOptions) (*PowershellContextCredential, error) {
cp := PowershellContextCredentialOptions{}
if options != nil {
cp = *options
}
if cp.TenantID != "" && !validTenantID(cp.TenantID) {
return nil, errors.New("invalid tenant id")
}
if cp.tokenProvider == nil {
cp.tokenProvider = defaultAzdTokenProvider
}
return &PowershellContextCredential{mu: &sync.Mutex{}, opts: cp}, nil
}

// GetToken requests a token from the Azure Developer CLI. This credential doesn't cache tokens, so every call invokes azd.
// This method is called automatically by Azure SDK clients.
func (c *PowershellContextCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
at := azcore.AccessToken{}
if len(opts.Scopes) != 1 {
return at, errors.New(credNamePSContext + ": GetToken() exactly one scope")
}

tenant, err := resolveTenant(c.opts.TenantID, opts.TenantID, credNamePSContext, nil)
if err != nil {
return at, err
}
c.mu.Lock()
defer c.mu.Unlock()
b, err := c.opts.tokenProvider(ctx, opts.Scopes[0], tenant)
if err == nil {
at, err = c.createAccessToken(b)
}
if err != nil {
return at, err
}
//msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", credNamePSContext, strings.Join(opts.Scopes, ", "))
return at, nil
}

// We ignore resource because PS does not support all Resources. Disk scope is not supported
// and we are here only with Storage scope
var defaultAzdTokenProvider PSTokenProvider = func(ctx context.Context, _ string, tenantID string) ([]byte, error) {
// set a default timeout for this authentication iff the application hasn't done so already
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
ctx, cancel = context.WithTimeout(ctx, 10 * time.Minute)
defer cancel()
}

r := regexp.MustCompile(`\{"token".*"expiresOn".*\}`)

if tenantID != "" {
tenantID += " -TenantId " + tenantID
}
cmd := `$token = Get-AzAccessToken -ResourceUrl https://storage.azure.com` + tenantID + ";"
cmd += `$output = -join('{"token":','"',$token.Token,'"', ',"expiresOn":', '"',$token.ExpiresOn.ToString("yyyy-MM-ddTHH:mm:ss.fffK"),'"',"}");`
cmd += "echo $output"

cliCmd := exec.CommandContext(ctx, "powershell", cmd)
cliCmd.Env = os.Environ()
var stderr bytes.Buffer
cliCmd.Stderr = &stderr

output, err := cliCmd.Output()
if err != nil {
msg := stderr.String()
if msg == "" {
msg = err.Error()
}
return nil, errors.New(credNamePSContext + msg)
}

output = []byte(r.FindString(string(output)))
if string(output) == "" {
return nil, errors.New(credNamePSContext + "Invalid output while retrving token")
}
return output, nil
}

func (c *PowershellContextCredential) createAccessToken(tk []byte) (azcore.AccessToken, error) {
t := struct {
AccessToken string `json:"token"`
ExpiresOn string `json:"expiresOn"`
}{}

err := json.Unmarshal(tk, &t)
if err != nil {
return azcore.AccessToken{}, errors.New(err.Error())
}

parseErr := "error parsing token expiration time %q: %v"
exp, err := time.Parse(time.RFC3339, t.ExpiresOn)
if err != nil {
return azcore.AccessToken{}, fmt.Errorf(parseErr, t.ExpiresOn, err)
}
return azcore.AccessToken{
ExpiresOn: exp.UTC(),
Token: t.AccessToken,
}, nil
}

var _ azcore.TokenCredential = (*PowershellContextCredential)(nil)
Loading

0 comments on commit 26640e7

Please sign in to comment.