1package cliconfig 2 3import ( 4 "io/ioutil" 5 "net/http" 6 "os" 7 "path/filepath" 8 "testing" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/zclconf/go-cty/cty" 12 13 "github.com/hashicorp/terraform-svchost" 14 svcauth "github.com/hashicorp/terraform-svchost/auth" 15) 16 17func TestCredentialsForHost(t *testing.T) { 18 credSrc := &CredentialsSource{ 19 configured: map[svchost.Hostname]cty.Value{ 20 "configured.example.com": cty.ObjectVal(map[string]cty.Value{ 21 "token": cty.StringVal("configured"), 22 }), 23 "unused.example.com": cty.ObjectVal(map[string]cty.Value{ 24 "token": cty.StringVal("incorrectly-configured"), 25 }), 26 }, 27 28 // We'll use a static source to stand in for what would normally be 29 // a credentials helper program, since we're only testing the logic 30 // for choosing when to delegate to the helper here. The logic for 31 // interacting with a helper program is tested in the svcauth package. 32 helper: svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ 33 "from-helper.example.com": { 34 "token": "from-helper", 35 }, 36 37 // This should be shadowed by the "configured" entry with the same 38 // hostname above. 39 "configured.example.com": { 40 "token": "incorrectly-from-helper", 41 }, 42 }), 43 helperType: "fake", 44 } 45 46 testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string { 47 t.Helper() 48 49 if creds == nil { 50 return "" 51 } 52 53 req, err := http.NewRequest("GET", "http://example.com/", nil) 54 if err != nil { 55 t.Fatalf("cannot construct HTTP request: %s", err) 56 } 57 creds.PrepareRequest(req) 58 return req.Header.Get("Authorization") 59 } 60 61 t.Run("configured", func(t *testing.T) { 62 creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com")) 63 if err != nil { 64 t.Fatalf("unexpected error: %s", err) 65 } 66 if got, want := testReqAuthHeader(t, creds), "Bearer configured"; got != want { 67 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 68 } 69 }) 70 t.Run("from helper", func(t *testing.T) { 71 creds, err := credSrc.ForHost(svchost.Hostname("from-helper.example.com")) 72 if err != nil { 73 t.Fatalf("unexpected error: %s", err) 74 } 75 if got, want := testReqAuthHeader(t, creds), "Bearer from-helper"; got != want { 76 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 77 } 78 }) 79 t.Run("not available", func(t *testing.T) { 80 creds, err := credSrc.ForHost(svchost.Hostname("unavailable.example.com")) 81 if err != nil { 82 t.Fatalf("unexpected error: %s", err) 83 } 84 if got, want := testReqAuthHeader(t, creds), ""; got != want { 85 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 86 } 87 }) 88} 89 90func TestCredentialsStoreForget(t *testing.T) { 91 d, err := ioutil.TempDir("", "terraform-cliconfig-test") 92 if err != nil { 93 t.Fatal(err) 94 } 95 defer os.RemoveAll(d) 96 97 mockCredsFilename := filepath.Join(d, "credentials.tfrc.json") 98 99 cfg := &Config{ 100 // This simulates there being a credentials block manually configured 101 // in some file _other than_ credentials.tfrc.json. 102 Credentials: map[string]map[string]interface{}{ 103 "manually-configured.example.com": { 104 "token": "manually-configured", 105 }, 106 }, 107 } 108 109 // We'll initially use a credentials source with no credentials helper at 110 // all, and thus with credentials stored in the credentials file. 111 credSrc := cfg.credentialsSource( 112 "", nil, 113 mockCredsFilename, 114 ) 115 116 testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string { 117 t.Helper() 118 119 if creds == nil { 120 return "" 121 } 122 123 req, err := http.NewRequest("GET", "http://example.com/", nil) 124 if err != nil { 125 t.Fatalf("cannot construct HTTP request: %s", err) 126 } 127 creds.PrepareRequest(req) 128 return req.Header.Get("Authorization") 129 } 130 131 // Because these store/forget calls have side-effects, we'll bail out with 132 // t.Fatal (or equivalent) as soon as anything unexpected happens. 133 // Otherwise downstream tests might fail in confusing ways. 134 { 135 err := credSrc.StoreForHost( 136 svchost.Hostname("manually-configured.example.com"), 137 svcauth.HostCredentialsToken("not-manually-configured"), 138 ) 139 if err == nil { 140 t.Fatalf("successfully stored for manually-configured; want error") 141 } 142 if _, ok := err.(ErrUnwritableHostCredentials); !ok { 143 t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err) 144 } 145 } 146 { 147 err := credSrc.ForgetForHost( 148 svchost.Hostname("manually-configured.example.com"), 149 ) 150 if err == nil { 151 t.Fatalf("successfully forgot for manually-configured; want error") 152 } 153 if _, ok := err.(ErrUnwritableHostCredentials); !ok { 154 t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err) 155 } 156 } 157 { 158 // We don't have a credentials file at all yet, so this first call 159 // must create it. 160 err := credSrc.StoreForHost( 161 svchost.Hostname("stored-locally.example.com"), 162 svcauth.HostCredentialsToken("stored-locally"), 163 ) 164 if err != nil { 165 t.Fatalf("unexpected error storing locally: %s", err) 166 } 167 168 creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) 169 if err != nil { 170 t.Fatalf("failed to read back stored-locally credentials: %s", err) 171 } 172 173 if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally"; got != want { 174 t.Fatalf("wrong header value for stored-locally\ngot: %s\nwant: %s", got, want) 175 } 176 177 got := readHostsInCredentialsFile(mockCredsFilename) 178 want := map[svchost.Hostname]struct{}{ 179 svchost.Hostname("stored-locally.example.com"): struct{}{}, 180 } 181 if diff := cmp.Diff(want, got); diff != "" { 182 t.Fatalf("wrong credentials file content\n%s", diff) 183 } 184 } 185 186 // Now we'll switch to having a credential helper active. 187 // If we were loading the real CLI config from disk here then this 188 // entry would already be in cfg.Credentials, but we need to fake that 189 // in the test because we're constructing this *Config value directly. 190 cfg.Credentials["stored-locally.example.com"] = map[string]interface{}{ 191 "token": "stored-locally", 192 } 193 mockHelper := &mockCredentialsHelper{current: make(map[svchost.Hostname]cty.Value)} 194 credSrc = cfg.credentialsSource( 195 "mock", mockHelper, 196 mockCredsFilename, 197 ) 198 { 199 err := credSrc.StoreForHost( 200 svchost.Hostname("manually-configured.example.com"), 201 svcauth.HostCredentialsToken("not-manually-configured"), 202 ) 203 if err == nil { 204 t.Fatalf("successfully stored for manually-configured with helper active; want error") 205 } 206 } 207 { 208 err := credSrc.StoreForHost( 209 svchost.Hostname("stored-in-helper.example.com"), 210 svcauth.HostCredentialsToken("stored-in-helper"), 211 ) 212 if err != nil { 213 t.Fatalf("unexpected error storing in helper: %s", err) 214 } 215 216 creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com")) 217 if err != nil { 218 t.Fatalf("failed to read back stored-in-helper credentials: %s", err) 219 } 220 221 if got, want := testReqAuthHeader(t, creds), "Bearer stored-in-helper"; got != want { 222 t.Fatalf("wrong header value for stored-in-helper\ngot: %s\nwant: %s", got, want) 223 } 224 225 // Nothing should have changed in the saved credentials file 226 got := readHostsInCredentialsFile(mockCredsFilename) 227 want := map[svchost.Hostname]struct{}{ 228 svchost.Hostname("stored-locally.example.com"): struct{}{}, 229 } 230 if diff := cmp.Diff(want, got); diff != "" { 231 t.Fatalf("wrong credentials file content\n%s", diff) 232 } 233 } 234 { 235 // Because stored-locally is already in the credentials file, a new 236 // store should be sent there rather than to the credentials helper. 237 err := credSrc.StoreForHost( 238 svchost.Hostname("stored-locally.example.com"), 239 svcauth.HostCredentialsToken("stored-locally-again"), 240 ) 241 if err != nil { 242 t.Fatalf("unexpected error storing locally again: %s", err) 243 } 244 245 creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) 246 if err != nil { 247 t.Fatalf("failed to read back stored-locally credentials: %s", err) 248 } 249 250 if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally-again"; got != want { 251 t.Fatalf("wrong header value for stored-locally\ngot: %s\nwant: %s", got, want) 252 } 253 } 254 { 255 // Forgetting a host already in the credentials file should remove it 256 // from the credentials file, not from the helper. 257 err := credSrc.ForgetForHost( 258 svchost.Hostname("stored-locally.example.com"), 259 ) 260 if err != nil { 261 t.Fatalf("unexpected error forgetting locally: %s", err) 262 } 263 264 creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) 265 if err != nil { 266 t.Fatalf("failed to read back stored-locally credentials: %s", err) 267 } 268 269 if got, want := testReqAuthHeader(t, creds), ""; got != want { 270 t.Fatalf("wrong header value for stored-locally\ngot: %s\nwant: %s", got, want) 271 } 272 273 // Should not be present in the credentials file anymore 274 got := readHostsInCredentialsFile(mockCredsFilename) 275 want := map[svchost.Hostname]struct{}{} 276 if diff := cmp.Diff(want, got); diff != "" { 277 t.Fatalf("wrong credentials file content\n%s", diff) 278 } 279 } 280 { 281 err := credSrc.ForgetForHost( 282 svchost.Hostname("stored-in-helper.example.com"), 283 ) 284 if err != nil { 285 t.Fatalf("unexpected error forgetting in helper: %s", err) 286 } 287 288 creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com")) 289 if err != nil { 290 t.Fatalf("failed to read back stored-in-helper credentials: %s", err) 291 } 292 293 if got, want := testReqAuthHeader(t, creds), ""; got != want { 294 t.Fatalf("wrong header value for stored-in-helper\ngot: %s\nwant: %s", got, want) 295 } 296 } 297 298 { 299 // Finally, the log in our mock helper should show that it was only 300 // asked to deal with stored-in-helper, not stored-locally. 301 got := mockHelper.log 302 want := []mockCredentialsHelperChange{ 303 { 304 Host: svchost.Hostname("stored-in-helper.example.com"), 305 Action: "store", 306 }, 307 { 308 Host: svchost.Hostname("stored-in-helper.example.com"), 309 Action: "forget", 310 }, 311 } 312 if diff := cmp.Diff(want, got); diff != "" { 313 t.Errorf("unexpected credentials helper operation log\n%s", diff) 314 } 315 } 316} 317 318type mockCredentialsHelperChange struct { 319 Host svchost.Hostname 320 Action string 321} 322 323type mockCredentialsHelper struct { 324 current map[svchost.Hostname]cty.Value 325 log []mockCredentialsHelperChange 326} 327 328// Assertion that mockCredentialsHelper implements svcauth.CredentialsSource 329var _ svcauth.CredentialsSource = (*mockCredentialsHelper)(nil) 330 331func (s *mockCredentialsHelper) ForHost(hostname svchost.Hostname) (svcauth.HostCredentials, error) { 332 v, ok := s.current[hostname] 333 if !ok { 334 return nil, nil 335 } 336 return svcauth.HostCredentialsFromObject(v), nil 337} 338 339func (s *mockCredentialsHelper) StoreForHost(hostname svchost.Hostname, new svcauth.HostCredentialsWritable) error { 340 s.log = append(s.log, mockCredentialsHelperChange{ 341 Host: hostname, 342 Action: "store", 343 }) 344 s.current[hostname] = new.ToStore() 345 return nil 346} 347 348func (s *mockCredentialsHelper) ForgetForHost(hostname svchost.Hostname) error { 349 s.log = append(s.log, mockCredentialsHelperChange{ 350 Host: hostname, 351 Action: "forget", 352 }) 353 delete(s.current, hostname) 354 return nil 355} 356