svchost/auth: New API for storing and forgetting credentials

This new functionality will be used as part of implementing the
"terraform login" and "terraform logout" commands.

As of this commit, the storage codepaths are all just stubs. Subsequent
commits will implement these new methods for each of the different
physical credentials sources.
This commit is contained in:
Martin Atkins 2019-07-30 15:40:26 -07:00
parent f3fe3bfb5f
commit 821d0401bc
6 changed files with 126 additions and 6 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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)),
})
}

View File

@ -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)
}
}
}