Skip to content

Commit c427506

Browse files
committed
Add testing for persistence and tenantID specification changes
1 parent 65a3780 commit c427506

13 files changed

+227
-26
lines changed

cmd/credentialUtil.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ func GetUserOAuthTokenManagerInstance() *common.UserOAuthTokenManager {
7070
if common.AzcopyJobPlanFolder == "" {
7171
panic("invalid state, AzcopyJobPlanFolder should not be an empty string")
7272
}
73+
cacheName := common.GetEnvironmentVariable(common.EEnvironmentVariable.LoginCacheName())
74+
7375
currentUserOAuthTokenManager = common.NewUserOAuthTokenManagerInstance(common.CredCacheOptions{
7476
DPAPIFilePath: common.AzcopyJobPlanFolder,
75-
KeyName: oauthLoginSessionCacheKeyName,
77+
KeyName: common.Iff(cacheName != "", cacheName, oauthLoginSessionCacheKeyName),
7678
ServiceName: oauthLoginSessionCacheServiceName,
77-
AccountName: oauthLoginSessionCacheAccountName,
79+
AccountName: common.Iff(cacheName != "", cacheName, oauthLoginSessionCacheAccountName),
7880
})
7981
})
8082

cmd/loginStatus.go

+46-7
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,25 @@ package cmd
2222

2323
import (
2424
"context"
25+
"encoding/json"
2526
"fmt"
2627
"github.com/Azure/azure-storage-azcopy/v10/common"
2728
"github.com/Azure/azure-storage-azcopy/v10/ste"
2829
"github.com/spf13/cobra"
2930
)
3031

32+
type LoginStatusOutput struct {
33+
Valid bool `json:"valid"`
34+
TenantID *string `json:"tenantID,omitempty"`
35+
AADEndpoint *string `json:"AADEndpoint,omitempty"`
36+
AuthMethod *string `json:"authMethod,omitempty"`
37+
}
38+
3139
func init() {
3240
type loginStatus struct {
3341
tenantID bool
3442
endpoint bool
43+
method bool
3544
}
3645
commandLineInput := loginStatus{}
3746

@@ -51,26 +60,56 @@ func init() {
5160
uotm := GetUserOAuthTokenManagerInstance()
5261
tokenInfo, err := uotm.GetTokenInfo(ctx)
5362

54-
if err == nil && !tokenInfo.IsExpired() {
55-
glcm.Info("You have successfully refreshed your token. Your login session is still active")
63+
var Info = LoginStatusOutput{
64+
Valid: err == nil && !tokenInfo.IsExpired(),
65+
}
66+
67+
logText := func(format string, a ...any) {
68+
if azcopyOutputFormat == common.EOutputFormat.None() || azcopyOutputFormat == common.EOutputFormat.Text() {
69+
glcm.Info(fmt.Sprintf(format, a...))
70+
}
71+
}
72+
73+
if Info.Valid {
74+
logText("You have successfully refreshed your token. Your login session is still active")
5675

5776
if commandLineInput.tenantID {
58-
glcm.Info(fmt.Sprintf("Tenant ID: %v", tokenInfo.Tenant))
77+
logText("Tenant ID: %v", tokenInfo.Tenant)
78+
Info.TenantID = &tokenInfo.Tenant
5979
}
6080

6181
if commandLineInput.endpoint {
62-
glcm.Info(fmt.Sprintf("Active directory endpoint: %v", tokenInfo.ActiveDirectoryEndpoint))
82+
logText(fmt.Sprintf("Active directory endpoint: %v", tokenInfo.ActiveDirectoryEndpoint))
83+
Info.AADEndpoint = &tokenInfo.ActiveDirectoryEndpoint
84+
}
85+
86+
if commandLineInput.method {
87+
logText(fmt.Sprintf("Authorized using %s", tokenInfo.LoginType))
88+
method := tokenInfo.LoginType.String()
89+
Info.AuthMethod = &method
6390
}
91+
} else {
92+
logText("You are currently not logged in. Please login using 'azcopy login'")
93+
}
94+
95+
if azcopyOutputFormat == common.EOutputFormat.Json() {
96+
glcm.Output(
97+
func(_ common.OutputFormat) string {
98+
buf, err := json.Marshal(Info)
99+
if err != nil {
100+
panic(err)
101+
}
64102

65-
glcm.Exit(nil, common.EExitCode.Success())
103+
return string(buf)
104+
}, common.EOutputMessageType.LoginStatusInfo())
66105
}
67106

68-
glcm.Info("You are currently not logged in. Please login using 'azcopy login'")
69-
glcm.Exit(nil, common.EExitCode.Error())
107+
glcm.Exit(nil, common.Iff(Info.Valid, common.EExitCode.Success(), common.EExitCode.Error()))
70108
},
71109
}
72110

73111
lgCmd.AddCommand(lgStatus)
74112
lgStatus.PersistentFlags().BoolVar(&commandLineInput.tenantID, "tenant", false, "Prints the Microsoft Entra tenant ID that is currently being used in session.")
75113
lgStatus.PersistentFlags().BoolVar(&commandLineInput.endpoint, "endpoint", false, "Prints the Microsoft Entra endpoint that is being used in the current session.")
114+
lgStatus.PersistentFlags().BoolVar(&commandLineInput.method, "method", false, "Prints the authorization method used in the current session.")
76115
}

common/credCache_windows.go

+4
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ func (c *CredCache) saveTokenInternal(token OAuthTokenInfo) error {
197197
}
198198

199199
func (c *CredCache) tokenFilePath() string {
200+
if cacheFile := GetEnvironmentVariable(EEnvironmentVariable.LoginCacheName()); cacheFile != "" {
201+
return path.Join(c.dpapiFilePath, "/", cacheFile)
202+
}
203+
200204
return path.Join(c.dpapiFilePath, "/", defaultTokenFileName)
201205
}
202206

common/environment.go

+7
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,13 @@ func (EnvironmentVariable) CacheProxyLookup() EnvironmentVariable {
306306
}
307307
}
308308

309+
func (EnvironmentVariable) LoginCacheName() EnvironmentVariable {
310+
return EnvironmentVariable{
311+
Name: "AZCOPY_LOGIN_CACHE_NAME",
312+
Description: "Do not use in production. Overrides the file name or key name used to cache azcopy's token. Do not use in production. This feature is not documented, intended for testing, and may break. Do not use in production.",
313+
}
314+
}
315+
309316
func (EnvironmentVariable) LogLocation() EnvironmentVariable {
310317
return EnvironmentVariable{
311318
Name: "AZCOPY_LOG_LOCATION",

common/oauthTokenManager.go

+8
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ func (credInfo *OAuthTokenInfo) GetClientSecretCredential() (azcore.TokenCredent
645645
}
646646

647647
func (credInfo *OAuthTokenInfo) GetAzCliCredential() (azcore.TokenCredential, error) {
648+
if credInfo.Tenant == DefaultTenantID {
649+
credInfo.Tenant = ""
650+
}
651+
648652
tc, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: credInfo.Tenant})
649653
if err != nil {
650654
return nil, err
@@ -654,6 +658,10 @@ func (credInfo *OAuthTokenInfo) GetAzCliCredential() (azcore.TokenCredential, er
654658
}
655659

656660
func (credInfo *OAuthTokenInfo) GetPSContextCredential() (azcore.TokenCredential, error) {
661+
if credInfo.Tenant == DefaultTenantID {
662+
credInfo.Tenant = ""
663+
}
664+
657665
tc, err := NewPowershellContextCredential(&PowershellContextCredentialOptions{TenantID: credInfo.Tenant})
658666
if err != nil {
659667
return nil, err

common/output.go

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ func (OutputMessageType) Response() OutputMessageType { return OutputMessageType
3636
func (OutputMessageType) ListObject() OutputMessageType { return OutputMessageType(8) }
3737
func (OutputMessageType) ListSummary() OutputMessageType { return OutputMessageType(9) }
3838

39+
func (OutputMessageType) LoginStatusInfo() OutputMessageType { return OutputMessageType(10) }
40+
3941
func (o OutputMessageType) String() string {
4042
return enum.StringInt(o, reflect.TypeOf(o))
4143
}

e2etest/newe2e_config.go

+13
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ func (e NewE2EConfig) GetSPNOptions() (present bool, tenant, applicationId, secr
110110
}
111111
}
112112

113+
func (e NewE2EConfig) GetTenantID() string {
114+
if e.StaticResources() {
115+
return e.E2EAuthConfig.StaticStgAcctInfo.StaticOAuth.TenantID
116+
} else {
117+
dynamicInfo := e.E2EAuthConfig.SubscriptionLoginInfo.DynamicOAuth
118+
if tid := dynamicInfo.SPNSecret.TenantID; tid != "" {
119+
return tid
120+
} else {
121+
return dynamicInfo.Workload.TenantId // worst case if it bubbles down and it's all zero, that's OK.
122+
}
123+
}
124+
}
125+
113126
// ========= Tag Definition ==========
114127

115128
type EnvTag struct {

e2etest/newe2e_runazcopy_stdout.go

+23
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,26 @@ func (a *AzCopyParsedJobsListStdout) Write(p []byte) (n int, err error) {
239239
}
240240
return a.AzCopyParsedStdout.Write(p)
241241
}
242+
243+
type AzCopyParsedLoginStatusStdout struct {
244+
AzCopyParsedStdout
245+
listenChan chan<- common.JsonOutputTemplate
246+
status cmd.LoginStatusOutput
247+
}
248+
249+
func (a *AzCopyParsedLoginStatusStdout) Write(p []byte) (n int, err error) {
250+
if a.listenChan == nil {
251+
a.listenChan = a.OnParsedLine.SubscribeFunc(func(line common.JsonOutputTemplate) {
252+
if line.MessageType == common.EOutputMessageType.LoginStatusInfo().String() {
253+
out := &cmd.LoginStatusOutput{}
254+
err = json.Unmarshal([]byte(line.MessageContent), out)
255+
if err != nil {
256+
return
257+
}
258+
259+
a.status = *out
260+
}
261+
})
262+
}
263+
return a.AzCopyParsedStdout.Write(p)
264+
}

e2etest/newe2e_task_runazcopy.go

+18-10
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,16 @@ var _ AzCopyStdout = &AzCopyRawStdout{}
5454
type AzCopyVerb string
5555

5656
const ( // initially supporting a limited set of verbs
57-
AzCopyVerbCopy AzCopyVerb = "copy"
58-
AzCopyVerbSync AzCopyVerb = "sync"
59-
AzCopyVerbRemove AzCopyVerb = "remove"
60-
AzCopyVerbList AzCopyVerb = "list"
61-
AzCopyVerbLogin AzCopyVerb = "login"
62-
AzCopyVerbLogout AzCopyVerb = "logout"
63-
AzCopyVerbJobs AzCopyVerb = "jobs"
57+
AzCopyVerbCopy AzCopyVerb = "copy"
58+
AzCopyVerbSync AzCopyVerb = "sync"
59+
AzCopyVerbRemove AzCopyVerb = "remove"
60+
AzCopyVerbList AzCopyVerb = "list"
61+
AzCopyVerbLogin AzCopyVerb = "login"
62+
AzCopyVerbLoginStatus AzCopyVerb = "login status"
63+
AzCopyVerbLogout AzCopyVerb = "logout"
64+
AzCopyVerbJobsList AzCopyVerb = "jobs list"
65+
AzCopyVerbJobsResume AzCopyVerb = "jobs resume"
66+
AzCopyVerbJobsClean AzCopyVerb = "jobs clean"
6467
)
6568

6669
type AzCopyTarget struct {
@@ -126,6 +129,8 @@ type AzCopyEnvironment struct {
126129
AzureTenantId *string `env:"AZURE_TENANT_ID"`
127130
AzureClientId *string `env:"AZURE_CLIENT_ID"`
128131

132+
LoginCacheName *string `env:"AZCOPY_LOGIN_CACHE_NAME"`
133+
129134
InheritEnvironment bool
130135
ManualLogin bool
131136

@@ -281,7 +286,8 @@ func RunAzCopy(a ScenarioAsserter, commandSpec AzCopyCommand) (AzCopyStdout, *Az
281286
commandSpec.Environment = &AzCopyEnvironment{}
282287
}
283288

284-
out := []string{GlobalConfig.AzCopyExecutableConfig.ExecutablePath, string(commandSpec.Verb)}
289+
out := []string{GlobalConfig.AzCopyExecutableConfig.ExecutablePath}
290+
out = append(out, strings.Split(string(commandSpec.Verb), " ")...)
285291

286292
for _, v := range commandSpec.PositionalArgs {
287293
out = append(out, v)
@@ -340,13 +346,15 @@ func RunAzCopy(a ScenarioAsserter, commandSpec AzCopyCommand) (AzCopyStdout, *Az
340346
}
341347
case commandSpec.Verb == AzCopyVerbList:
342348
out = &AzCopyParsedListStdout{}
343-
case commandSpec.Verb == AzCopyVerbJobs && len(commandSpec.PositionalArgs) != 0 && commandSpec.PositionalArgs[0] == "list":
349+
case commandSpec.Verb == AzCopyVerbJobsList:
344350
out = &AzCopyParsedJobsListStdout{}
345-
case commandSpec.Verb == AzCopyVerbJobs && len(commandSpec.PositionalArgs) != 0 && commandSpec.PositionalArgs[0] == "resume":
351+
case commandSpec.Verb == AzCopyVerbJobsResume:
346352
out = &AzCopyParsedCopySyncRemoveStdout{ // Resume command treated the same as copy/sync/remove
347353
JobPlanFolder: *commandSpec.Environment.JobPlanLocation,
348354
LogFolder: *commandSpec.Environment.LogLocation,
349355
}
356+
case commandSpec.Verb == AzCopyVerbLoginStatus:
357+
out = &AzCopyParsedLoginStatusStdout{}
350358

351359
default: // We don't know how to parse this.
352360
out = &AzCopyRawStdout{}

e2etest/newe2e_task_runazcopy_parameters.go

+25
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,31 @@ type ListFlags struct {
417417
TrailingDot *common.TrailingDotOption `flag:"trailing-dot"`
418418
}
419419

420+
type LoginFlags struct {
421+
GlobalFlags
422+
423+
// Generic flags
424+
TenantID *string `flag:"tenant-id"`
425+
AADEndpoint *string `flag:"aad-endpoint"`
426+
LoginType *common.AutoLoginType `flag:"login-type"`
427+
428+
// Managed identity
429+
IdentityClientID *string `flag:"identity-client-id"`
430+
IdentityResourceID *string `flag:"identity-resource-id"`
431+
432+
// SPN
433+
ApplicationID *string `flag:"application-id"`
434+
CertPath *string `flag:"certificate-path"`
435+
}
436+
437+
type LoginStatusFlags struct {
438+
GlobalFlags
439+
440+
Tenant *bool `flag:"tenant"`
441+
Endpoint *bool `flag:"endpoint"`
442+
Method *bool `flag:"method"`
443+
}
444+
420445
type WindowsAttribute uint32
421446

422447
const (
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package e2etest
2+
3+
import (
4+
"fmt"
5+
"github.com/Azure/azure-storage-azcopy/v10/common"
6+
)
7+
8+
func init() {
9+
suiteManager.RegisterSuite(&TokenPersistenceSuite{})
10+
}
11+
12+
type TokenPersistenceSuite struct{}
13+
14+
func (*TokenPersistenceSuite) Scenario_InheritCred_Persist(a *ScenarioVariationManager) {
15+
credSource := ResolveVariation(a, []common.AutoLoginType{common.EAutoLoginType.PsCred(), common.EAutoLoginType.AzCLI()})
16+
withSpecifiedTenantID := NamedResolveVariation(a, map[string]bool{
17+
"-withTenantID": true,
18+
"": false,
19+
})
20+
cfgTenantID := GlobalConfig.GetTenantID()
21+
22+
azcopyEnv := &AzCopyEnvironment{
23+
LoginCacheName: pointerTo(fmt.Sprintf("AzCopyPersist%sTest", credSource.String())),
24+
InheritEnvironment: true, // we want the executables in PATH
25+
ManualLogin: true,
26+
}
27+
28+
_, _ = RunAzCopy(a,
29+
AzCopyCommand{
30+
Verb: AzCopyVerbLogin,
31+
Flags: LoginFlags{
32+
LoginType: &credSource,
33+
TenantID: common.Iff(withSpecifiedTenantID && cfgTenantID != "", &cfgTenantID, nil),
34+
},
35+
Environment: azcopyEnv,
36+
})
37+
38+
loginStatus, _ := RunAzCopy(a,
39+
AzCopyCommand{
40+
Verb: AzCopyVerbLoginStatus,
41+
Flags: LoginStatusFlags{
42+
Method: pointerTo(true),
43+
Endpoint: pointerTo(true),
44+
Tenant: pointerTo(true),
45+
},
46+
Environment: azcopyEnv,
47+
})
48+
49+
parsedStdout, ok := loginStatus.(*AzCopyParsedLoginStatusStdout)
50+
a.AssertNow("must be AzCopyParsedLoginStatusStdout", Equal{}, ok, true)
51+
52+
if !a.Dryrun() {
53+
status := parsedStdout.status
54+
a.Assert("Login check failed", Equal{}, true, status.Valid)
55+
56+
a.Assert("Tenant not returned", Not{IsNil{}}, status.TenantID) // Let's just do a little extra testing while we're at it, kill two birds with one stone.
57+
if withSpecifiedTenantID && status.TenantID != nil && cfgTenantID != "" {
58+
a.Assert("Tenant does not match", Equal{}, cfgTenantID, *status.TenantID)
59+
}
60+
a.Assert("Endpoint not returned", Not{IsNil{}}, status.AADEndpoint)
61+
a.Assert("Auth mechanism not returned", Not{IsNil{}}, status.AuthMethod)
62+
if status.AuthMethod != nil {
63+
a.Assert("Incorrect auth mechanism", Equal{}, *status.AuthMethod, credSource.String())
64+
}
65+
}
66+
67+
_, _ = RunAzCopy(a,
68+
AzCopyCommand{
69+
Verb: AzCopyVerbLogout,
70+
Environment: azcopyEnv,
71+
})
72+
}

e2etest/zt_newe2e_jobs_clean_test.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ func (s *JobsCleanSuite) Scenario_JobsCleanBasic(svm *ScenarioVariationManager)
3434
jobsCleanOutput, _ := RunAzCopy(
3535
svm,
3636
AzCopyCommand{
37-
Verb: AzCopyVerbJobs,
38-
PositionalArgs: []string{"clean"},
39-
ShouldFail: false,
37+
Verb: AzCopyVerbJobsClean,
38+
ShouldFail: false,
4039
Environment: &AzCopyEnvironment{
4140
LogLocation: &logsDir,
4241
JobPlanLocation: &jobPlanDir,

0 commit comments

Comments
 (0)