diff --git a/svchost/auth/cache.go b/svchost/auth/cache.go index 4f0d1689f..509d89fd4 100644 --- a/svchost/auth/cache.go +++ b/svchost/auth/cache.go @@ -43,3 +43,19 @@ func (s *cachingCredentialsSource) ForHost(host svchost.Hostname) (HostCredentia s.cache[host] = result return result, nil } + +func (s *cachingCredentialsSource) StoreForHost(host svchost.Hostname, credentials HostCredentialsWritable) error { + // We'll delete the cache entry even if the store fails, since that just + // means that the next read will go to the real store and get a chance to + // see which object (old or new) is actually present. + delete(s.cache, host) + return s.source.StoreForHost(host, credentials) +} + +func (s *cachingCredentialsSource) ForgetForHost(host svchost.Hostname) error { + // We'll delete the cache entry even if the store fails, since that just + // means that the next read will go to the real store and get a chance to + // see if the object is still present. + delete(s.cache, host) + return s.source.ForgetForHost(host) +} diff --git a/svchost/auth/credentials.go b/svchost/auth/credentials.go index 0372c1609..d86492c54 100644 --- a/svchost/auth/credentials.go +++ b/svchost/auth/credentials.go @@ -3,8 +3,11 @@ package auth import ( + "fmt" "net/http" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/svchost" ) @@ -14,6 +17,9 @@ import ( // A Credentials is itself a CredentialsSource, wrapping its members. // In principle one CredentialsSource can be nested inside another, though // there is no good reason to do so. +// +// The write operations on a Credentials are tried only on the first object, +// under the assumption that it is the primary store. type Credentials []CredentialsSource // NoCredentials is an empty CredentialsSource that always returns nil @@ -33,6 +39,19 @@ type CredentialsSource interface { // If an error is returned, progress through a list of CredentialsSources // is halted and the error is returned to the user. ForHost(host svchost.Hostname) (HostCredentials, error) + + // StoreForHost takes a HostCredentialsWritable and saves it as the + // credentials for the given host. + // + // If credentials are already stored for the given host, it will try to + // replace those credentials but may produce an error if such replacement + // is not possible. + StoreForHost(host svchost.Hostname, credentials HostCredentialsWritable) error + + // ForgetForHost discards any stored credentials for the given host. It + // does nothing and returns successfully if no credentials are saved + // for that host. + ForgetForHost(host svchost.Hostname) error } // HostCredentials represents a single set of credentials for a particular @@ -47,6 +66,22 @@ type HostCredentials interface { Token() string } +// HostCredentialsWritable is an extension of HostCredentials for credentials +// objects that can be serialized as a JSON-compatible object value for +// storage. +type HostCredentialsWritable interface { + HostCredentials + + // ToStore returns a cty.Value, always of an object type, + // representing data that can be serialized to represent this object + // in persistent storage. + // + // The resulting value may uses only cty values that can be accepted + // by the cty JSON encoder, though the caller may elect to instead store + // it in some other format that has a JSON-compatible type system. + ToStore() cty.Value +} + // ForHost iterates over the contained CredentialsSource objects and // tries to obtain credentials for the given host from each one in turn. // @@ -61,3 +96,23 @@ func (c Credentials) ForHost(host svchost.Hostname) (HostCredentials, error) { } return nil, nil } + +// StoreForHost passes the given arguments to the same operation on the +// first CredentialsSource in the receiver. +func (c Credentials) StoreForHost(host svchost.Hostname, credentials HostCredentialsWritable) error { + if len(c) == 0 { + return fmt.Errorf("no credentials store is available") + } + + return c[0].StoreForHost(host, credentials) +} + +// ForgetForHost passes the given arguments to the same operation on the +// first CredentialsSource in the receiver. +func (c Credentials) ForgetForHost(host svchost.Hostname) error { + if len(c) == 0 { + return fmt.Errorf("no credentials store is available") + } + + return c[0].ForgetForHost(host) +} diff --git a/svchost/auth/helper_program.go b/svchost/auth/helper_program.go index d72ffe3c9..d1fb33fe8 100644 --- a/svchost/auth/helper_program.go +++ b/svchost/auth/helper_program.go @@ -78,3 +78,11 @@ func (s *helperProgramCredentialsSource) ForHost(host svchost.Hostname) (HostCre return HostCredentialsFromMap(m), nil } + +func (s *helperProgramCredentialsSource) StoreForHost(host svchost.Hostname, credentials HostCredentialsWritable) error { + return fmt.Errorf("credentials helper cannot currently store new credentials") +} + +func (s *helperProgramCredentialsSource) ForgetForHost(host svchost.Hostname) error { + return fmt.Errorf("credentials helper cannot currently forget existing credentials") +} diff --git a/svchost/auth/static.go b/svchost/auth/static.go index 5373fddfc..ba5252a2c 100644 --- a/svchost/auth/static.go +++ b/svchost/auth/static.go @@ -1,6 +1,8 @@ package auth import ( + "fmt" + "github.com/hashicorp/terraform/svchost" ) @@ -26,3 +28,11 @@ func (s staticCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials return nil, nil } + +func (s staticCredentialsSource) StoreForHost(host svchost.Hostname, credentials HostCredentialsWritable) error { + return fmt.Errorf("can't store new credentials in a static credentials source") +} + +func (s staticCredentialsSource) ForgetForHost(host svchost.Hostname) error { + return fmt.Errorf("can't discard credentials from a static credentials source") +} diff --git a/svchost/auth/token_credentials.go b/svchost/auth/token_credentials.go index 9358bcb64..1d36553ae 100644 --- a/svchost/auth/token_credentials.go +++ b/svchost/auth/token_credentials.go @@ -2,13 +2,23 @@ package auth import ( "net/http" + + "github.com/zclconf/go-cty/cty" ) // HostCredentialsToken is a HostCredentials implementation that represents a // single "bearer token", to be sent to the server via an Authorization header -// with the auth type set to "Bearer" +// with the auth type set to "Bearer". +// +// To save a token as the credentials for a host, convert the token string to +// this type and use the result as a HostCredentialsWritable implementation. type HostCredentialsToken string +// Interface implementation assertions. Compilation will fail here if +// HostCredentialsToken does not fully implement these interfaces. +var _ HostCredentials = HostCredentialsToken("") +var _ HostCredentialsWritable = HostCredentialsToken("") + // PrepareRequest alters the given HTTP request by setting its Authorization // header to the string "Bearer " followed by the encapsulated authentication // token. @@ -23,3 +33,11 @@ func (tc HostCredentialsToken) PrepareRequest(req *http.Request) { func (tc HostCredentialsToken) Token() string { return string(tc) } + +// ToStore returns a credentials object with a single attribute "token" whose +// value is the token string. +func (tc HostCredentialsToken) ToStore() cty.Value { + return cty.ObjectVal(map[string]cty.Value{ + "token": cty.StringVal(string(tc)), + }) +} diff --git a/svchost/auth/token_credentials_test.go b/svchost/auth/token_credentials_test.go index 3f7355063..61f2c9bd4 100644 --- a/svchost/auth/token_credentials_test.go +++ b/svchost/auth/token_credentials_test.go @@ -3,16 +3,29 @@ package auth import ( "net/http" "testing" + + "github.com/zclconf/go-cty/cty" ) func TestHostCredentialsToken(t *testing.T) { creds := HostCredentialsToken("foo-bar") - req := &http.Request{} - creds.PrepareRequest(req) + { + req := &http.Request{} + creds.PrepareRequest(req) + authStr := req.Header.Get("authorization") + if got, want := authStr, "Bearer foo-bar"; got != want { + t.Errorf("wrong Authorization header value %q; want %q", got, want) + } + } - authStr := req.Header.Get("authorization") - if got, want := authStr, "Bearer foo-bar"; got != want { - t.Errorf("wrong Authorization header value %q; want %q", got, want) + { + got := creds.ToStore() + want := cty.ObjectVal(map[string]cty.Value{ + "token": cty.StringVal("foo-bar"), + }) + if !want.RawEquals(got) { + t.Errorf("wrong storable object value\ngot: %#v\nwant: %#v", got, want) + } } }