From f3f7a5545d1700c1415736b9d0f6d1d40c2cd77f Mon Sep 17 00:00:00 2001 From: Chang You Date: Fri, 23 Jun 2023 11:44:09 -0700 Subject: [PATCH 01/11] Created handleJWTTokenRefreshEvent and retrieveJWTToken functions --- .../spire_producer.example.go | 171 ++++++ .../workloadapi/addr.go | 69 +++ .../workloadapi/addr_posix.go | 34 ++ .../workloadapi/addr_windows.go | 33 ++ .../workloadapi/backoff.go | 34 ++ .../workloadapi/bundlesource.go | 188 ++++++ .../workloadapi/client.go | 549 ++++++++++++++++++ .../workloadapi/client_posix.go | 29 + .../workloadapi/client_windows.go | 57 ++ .../workloadapi/convenience.go | 124 ++++ .../workloadapi/jwtsource.go | 109 ++++ .../workloadapi/option.go | 147 +++++ .../workloadapi/option_windows.go | 12 + .../workloadapi/watcher.go | 191 ++++++ .../workloadapi/x509context.go | 23 + .../workloadapi/x509source.go | 124 ++++ 16 files changed, 1894 insertions(+) create mode 100644 examples/spire_producer.example/spire_producer.example.go create mode 100644 examples/spire_producer.example/workloadapi/addr.go create mode 100644 examples/spire_producer.example/workloadapi/addr_posix.go create mode 100644 examples/spire_producer.example/workloadapi/addr_windows.go create mode 100644 examples/spire_producer.example/workloadapi/backoff.go create mode 100644 examples/spire_producer.example/workloadapi/bundlesource.go create mode 100644 examples/spire_producer.example/workloadapi/client.go create mode 100644 examples/spire_producer.example/workloadapi/client_posix.go create mode 100644 examples/spire_producer.example/workloadapi/client_windows.go create mode 100644 examples/spire_producer.example/workloadapi/convenience.go create mode 100644 examples/spire_producer.example/workloadapi/jwtsource.go create mode 100644 examples/spire_producer.example/workloadapi/option.go create mode 100644 examples/spire_producer.example/workloadapi/option_windows.go create mode 100644 examples/spire_producer.example/workloadapi/watcher.go create mode 100644 examples/spire_producer.example/workloadapi/x509context.go create mode 100644 examples/spire_producer.example/workloadapi/x509source.go diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go new file mode 100644 index 000000000..d7265870d --- /dev/null +++ b/examples/spire_producer.example/spire_producer.example.go @@ -0,0 +1,171 @@ +package main + +import ( + "context" + "fmt" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/spiffe/go-spiffe/v2/workloadapi" + "os" + "os/signal" + "regexp" + "syscall" + "time" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + _ "github.com/spiffe/go-spiffe/v2/spiffeid" + _ "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" +) + +var ( + // Regex for sasl.oauthbearer.config, which constrains it to be + // 1 or more name=value pairs with optional ignored whitespace + oauthbearerConfigRegex = regexp.MustCompile("^(\\s*(\\w+)\\s*=\\s*(\\w+))+\\s*$") + // Regex used to extract name=value pairs from sasl.oauthbearer.config + oauthbearerNameEqualsValueRegex = regexp.MustCompile("(\\w+)\\s*=\\s*(\\w+)") +) + +const ( + principalClaimNameKey = "principalClaimName" + principalKey = "principal" + joseHeaderEncoded = "eyJhbGciOiJub25lIn0" // {"alg":"none"} +) + +type tokenAuth struct { + audience []string + tokenSource *workloadapi.JWTSource +} + +// handleJWTTokenRefreshEvent retrieves JWT from the SPIRE workload API and +// sets the token on the client for use in any future authentication attempt. +// It must be invoked whenever kafka.OAuthBearerTokenRefresh appears on the client's event channel, +// which will occur whenever the client requires a token (i.e. when it first starts and when the +// previously-received token is 80% of the way to its expiration time). +func handleJWTTokenRefreshEvent(ctx context.Context, client kafka.Handle, principal, socketPath string, audience []string) { + fmt.Fprintf(os.Stderr, "Token refresh\n") + oauthBearerToken, retrieveErr := retrieveJWTToken(ctx, principal, socketPath, audience) + if retrieveErr != nil { + fmt.Fprintf(os.Stderr, "%% Token retrieval error: %v\n", retrieveErr) + client.SetOAuthBearerTokenFailure(retrieveErr.Error()) + } else { + setTokenError := client.SetOAuthBearerToken(oauthBearerToken) + if setTokenError != nil { + fmt.Fprintf(os.Stderr, "%% Error setting token and extensions: %v\n", setTokenError) + client.SetOAuthBearerTokenFailure(setTokenError.Error()) + } + } +} + +func retrieveJWTToken(ctx context.Context, principal, socketPath string, audience []string) (kafka.OAuthBearerToken, error) { + jwtSource, err := workloadapi.NewJWTSource( + ctx, + workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), + ) + if err != nil { + return kafka.OAuthBearerToken{}, fmt.Errorf("unable to create JWTSource: %w", err) + } + + defer jwtSource.Close() + + params := jwtsvid.Params{ + // initialize the fields of Params here + Audience: audience[0], + // Other fields... + } + + jwtSVID, err := jwtSource.FetchJWTSVID(ctx, params) + if err != nil { + return kafka.OAuthBearerToken{}, fmt.Errorf("unable to fetch JWT SVID: %w", err) + } + + oauthBearerToken := kafka.OAuthBearerToken{ + TokenValue: jwtSVID.Marshal(), + Expiration: jwtSVID.Expiry, + Principal: principal, + Extensions: map[string]string{}, + } + + return oauthBearerToken, nil +} + +func main() { + + if len(os.Args) != 5 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + bootstrapServers := os.Args[1] + topic := os.Args[2] + principal := os.Args[3] + socketPath := os.Args[4] + audience := []string{"audience1", "audience2"} // Audience should be defined properly + + // You'll probably need to modify this configuration to + // match your environment. + config := kafka.ConfigMap{ + "bootstrap.servers": bootstrapServers, + "security.protocol": "SASL_PLAINTEXT", + "sasl.mechanisms": "OAUTHBEARER", + "sasl.oauthbearer.config": map[string]string{ + "principal": principal, + }, + } + + p, err := kafka.NewProducer(&config) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create producer: %s\n", err) + os.Exit(1) + } + + // Token refresh events are posted on the Events channel, instructing + // the application to refresh its token. + ctx := context.Background() + go func(eventsChan chan kafka.Event) { + for ev := range eventsChan { + _, ok := ev.(kafka.OAuthBearerTokenRefresh) + if !ok { + // Ignore other event types + continue + } + + handleJWTTokenRefreshEvent(ctx, p, principal, socketPath, audience) + } + }(p.Events()) + + run := true + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) + + msgcnt := 0 + for run { + select { + case sig := <-signalChannel: + fmt.Printf("Caught signal %v: terminating\n", sig) + run = false + default: + value := fmt.Sprintf("Producer example, message #%d", msgcnt) + err = p.Produce(&kafka.Message{ + TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny}, + Value: []byte(value), + Headers: []kafka.Header{{Key: "myTestHeader", Value: []byte("header values are binary")}}, + }, nil) + + if err != nil { + if err.(kafka.Error).Code() == kafka.ErrQueueFull { + // Producer queue is full, wait 1s for messages + // to be delivered then try again. + time.Sleep(time.Second) + continue + } + fmt.Printf("Failed to produce message: %v\n", err) + } else { + fmt.Printf("Produced message: %s\n", value) + } + + time.Sleep(1 * time.Second) + msgcnt++ + } + } + + p.Close() +} diff --git a/examples/spire_producer.example/workloadapi/addr.go b/examples/spire_producer.example/workloadapi/addr.go new file mode 100644 index 000000000..6ce0238fe --- /dev/null +++ b/examples/spire_producer.example/workloadapi/addr.go @@ -0,0 +1,69 @@ +package workloadapi + +import ( + "errors" + "net" + "net/url" + "os" +) + +const ( + // SocketEnv is the environment variable holding the default Workload API + // address. + SocketEnv = "SPIFFE_ENDPOINT_SOCKET" +) + +func GetDefaultAddress() (string, bool) { + return os.LookupEnv(SocketEnv) +} + +// ValidateAddress validates that the provided address +// can be parsed to a gRPC target string for dialing +// a Workload API endpoint exposed as either a Unix +// Domain Socket or TCP socket. +func ValidateAddress(addr string) error { + _, err := parseTargetFromStringAddr(addr) + return err +} + +// parseTargetFromStringAddr parses the endpoint address and returns a gRPC target +// string for dialing. +func parseTargetFromStringAddr(addr string) (string, error) { + u, err := url.Parse(addr) + if err != nil { + return "", errors.New("workload endpoint socket is not a valid URI: " + err.Error()) + } + return parseTargetFromURLAddr(u) +} + +func parseTargetFromURLAddr(u *url.URL) (string, error) { + if u.Scheme == "tcp" { + switch { + case u.Opaque != "": + return "", errors.New("workload endpoint tcp socket URI must not be opaque") + case u.User != nil: + return "", errors.New("workload endpoint tcp socket URI must not include user info") + case u.Host == "": + return "", errors.New("workload endpoint tcp socket URI must include a host") + case u.Path != "": + return "", errors.New("workload endpoint tcp socket URI must not include a path") + case u.RawQuery != "": + return "", errors.New("workload endpoint tcp socket URI must not include query values") + case u.Fragment != "": + return "", errors.New("workload endpoint tcp socket URI must not include a fragment") + } + + ip := net.ParseIP(u.Hostname()) + if ip == nil { + return "", errors.New("workload endpoint tcp socket URI host component must be an IP:port") + } + port := u.Port() + if port == "" { + return "", errors.New("workload endpoint tcp socket URI host component must include a port") + } + + return net.JoinHostPort(ip.String(), port), nil + } + + return parseTargetFromURLAddrOS(u) +} diff --git a/examples/spire_producer.example/workloadapi/addr_posix.go b/examples/spire_producer.example/workloadapi/addr_posix.go new file mode 100644 index 000000000..0fa3f56b7 --- /dev/null +++ b/examples/spire_producer.example/workloadapi/addr_posix.go @@ -0,0 +1,34 @@ +//go:build !windows +// +build !windows + +package workloadapi + +import ( + "errors" + "net/url" +) + +var ( + ErrInvalidEndpointScheme = errors.New("workload endpoint socket URI must have a \"tcp\" or \"unix\" scheme") +) + +func parseTargetFromURLAddrOS(u *url.URL) (string, error) { + switch u.Scheme { + case "unix": + switch { + case u.Opaque != "": + return "", errors.New("workload endpoint unix socket URI must not be opaque") + case u.User != nil: + return "", errors.New("workload endpoint unix socket URI must not include user info") + case u.Host == "" && u.Path == "": + return "", errors.New("workload endpoint unix socket URI must include a path") + case u.RawQuery != "": + return "", errors.New("workload endpoint unix socket URI must not include query values") + case u.Fragment != "": + return "", errors.New("workload endpoint unix socket URI must not include a fragment") + } + return u.String(), nil + default: + return "", ErrInvalidEndpointScheme + } +} diff --git a/examples/spire_producer.example/workloadapi/addr_windows.go b/examples/spire_producer.example/workloadapi/addr_windows.go new file mode 100644 index 000000000..4f9f2c352 --- /dev/null +++ b/examples/spire_producer.example/workloadapi/addr_windows.go @@ -0,0 +1,33 @@ +//go:build windows +// +build windows + +package workloadapi + +import ( + "errors" + "net/url" +) + +var ( + ErrInvalidEndpointScheme = errors.New("workload endpoint socket URI must have a \"tcp\" or \"npipe\" scheme") +) + +func parseTargetFromURLAddrOS(u *url.URL) (string, error) { + switch u.Scheme { + case "npipe": + switch { + case u.Opaque == "" && u.Host != "": + return "", errors.New("workload endpoint named pipe URI must be opaque") + case u.Opaque == "": + return "", errors.New("workload endpoint named pipe URI must include an opaque part") + case u.RawQuery != "": + return "", errors.New("workload endpoint named pipe URI must not include query values") + case u.Fragment != "": + return "", errors.New("workload endpoint named pipe URI must not include a fragment") + } + + return namedPipeTarget(u.Opaque), nil + default: + return "", ErrInvalidEndpointScheme + } +} diff --git a/examples/spire_producer.example/workloadapi/backoff.go b/examples/spire_producer.example/workloadapi/backoff.go new file mode 100644 index 000000000..b6ef1ed53 --- /dev/null +++ b/examples/spire_producer.example/workloadapi/backoff.go @@ -0,0 +1,34 @@ +package workloadapi + +import ( + "math" + "time" +) + +// backoff defines an linear backoff policy. +type backoff struct { + InitialDelay time.Duration + MaxDelay time.Duration + n int +} + +func newBackoff() *backoff { + return &backoff{ + InitialDelay: time.Second, + MaxDelay: 30 * time.Second, + n: 0, + } +} + +// Duration returns the next wait period for the backoff. Not goroutine-safe. +func (b *backoff) Duration() time.Duration { + backoff := float64(b.n) + 1 + d := math.Min(b.InitialDelay.Seconds()*backoff, b.MaxDelay.Seconds()) + b.n++ + return time.Duration(d) * time.Second +} + +// Reset resets the backoff's state. +func (b *backoff) Reset() { + b.n = 0 +} diff --git a/examples/spire_producer.example/workloadapi/bundlesource.go b/examples/spire_producer.example/workloadapi/bundlesource.go new file mode 100644 index 000000000..2a253efc7 --- /dev/null +++ b/examples/spire_producer.example/workloadapi/bundlesource.go @@ -0,0 +1,188 @@ +package workloadapi + +import ( + "context" + "crypto" + "crypto/x509" + "sync" + + "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" + "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/zeebo/errs" +) + +var bundlesourceErr = errs.Class("bundlesource") + +// BundleSource is a source of SPIFFE bundles maintained via the Workload API. +type BundleSource struct { + watcher *watcher + + mtx sync.RWMutex + x509Authorities map[spiffeid.TrustDomain][]*x509.Certificate + jwtAuthorities map[spiffeid.TrustDomain]map[string]crypto.PublicKey + + closeMtx sync.RWMutex + closed bool +} + +// NewBundleSource creates a new BundleSource. It blocks until the initial +// update has been received from the Workload API. The source should be closed +// when no longer in use to free underlying resources. +func NewBundleSource(ctx context.Context, options ...BundleSourceOption) (_ *BundleSource, err error) { + config := &bundleSourceConfig{} + for _, option := range options { + option.configureBundleSource(config) + } + + s := &BundleSource{ + x509Authorities: make(map[spiffeid.TrustDomain][]*x509.Certificate), + jwtAuthorities: make(map[spiffeid.TrustDomain]map[string]crypto.PublicKey), + } + + s.watcher, err = newWatcher(ctx, config.watcher, s.setX509Context, s.setJWTBundles) + if err != nil { + return nil, err + } + + return s, nil +} + +// Close closes the source, dropping the connection to the Workload API. +// Other source methods will return an error after Close has been called. +// The underlying Workload API client will also be closed if it is owned by +// the BundleSource (i.e. not provided via the WithClient option). +func (s *BundleSource) Close() error { + s.closeMtx.Lock() + s.closed = true + s.closeMtx.Unlock() + + return s.watcher.Close() +} + +// GetBundleForTrustDomain returns the SPIFFE bundle for the given trust +// domain. It implements the spiffebundle.Source interface. +func (s *BundleSource) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*spiffebundle.Bundle, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + s.mtx.RLock() + defer s.mtx.RUnlock() + + x509Authorities, hasX509Authorities := s.x509Authorities[trustDomain] + jwtAuthorities, hasJWTAuthorities := s.jwtAuthorities[trustDomain] + if !hasX509Authorities && !hasJWTAuthorities { + return nil, bundlesourceErr.New("no SPIFFE bundle for trust domain %q", trustDomain) + } + bundle := spiffebundle.New(trustDomain) + if hasX509Authorities { + bundle.SetX509Authorities(x509Authorities) + } + if hasJWTAuthorities { + bundle.SetJWTAuthorities(jwtAuthorities) + } + return bundle, nil +} + +// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust +// domain. It implements the x509bundle.Source interface. +func (s *BundleSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + s.mtx.RLock() + defer s.mtx.RUnlock() + + x509Authorities, hasX509Authorities := s.x509Authorities[trustDomain] + if !hasX509Authorities { + return nil, bundlesourceErr.New("no X.509 bundle for trust domain %q", trustDomain) + } + return x509bundle.FromX509Authorities(trustDomain, x509Authorities), nil +} + +// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust +// domain. It implements the jwtbundle.Source interface. +func (s *BundleSource) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + s.mtx.RLock() + defer s.mtx.RUnlock() + + jwtAuthorities, hasJWTAuthorities := s.jwtAuthorities[trustDomain] + if !hasJWTAuthorities { + return nil, bundlesourceErr.New("no JWT bundle for trust domain %q", trustDomain) + } + return jwtbundle.FromJWTAuthorities(trustDomain, jwtAuthorities), nil +} + +// WaitUntilUpdated waits until the source is updated or the context is done, +// in which case ctx.Err() is returned. +func (s *BundleSource) WaitUntilUpdated(ctx context.Context) error { + return s.watcher.WaitUntilUpdated(ctx) +} + +// Updated returns a channel that is sent on whenever the source is updated. +func (s *BundleSource) Updated() <-chan struct{} { + return s.watcher.Updated() +} + +func (s *BundleSource) setX509Context(x509Context *X509Context) { + s.mtx.Lock() + defer s.mtx.Unlock() + + newBundles := x509Context.Bundles.Bundles() + + // Add/replace the X.509 authorities from the X.509 context. Track the trust + // domains represented in the new X.509 context so we can determine which + // existing trust domains are no longer represented. + trustDomains := make(map[spiffeid.TrustDomain]struct{}, len(newBundles)) + for _, newBundle := range newBundles { + trustDomains[newBundle.TrustDomain()] = struct{}{} + s.x509Authorities[newBundle.TrustDomain()] = newBundle.X509Authorities() + } + + // Remove the X.509 authority entries for trust domains no longer + // represented in the X.509 context. + for existingTD := range s.x509Authorities { + if _, ok := trustDomains[existingTD]; ok { + continue + } + delete(s.x509Authorities, existingTD) + } +} + +func (s *BundleSource) setJWTBundles(bundles *jwtbundle.Set) { + s.mtx.Lock() + defer s.mtx.Unlock() + + newBundles := bundles.Bundles() + + // Add/replace the JWT authorities from the JWT bundles. Track the trust + // domains represented in the new JWT bundles so we can determine which + // existing trust domains are no longer represented. + trustDomains := make(map[spiffeid.TrustDomain]struct{}, len(newBundles)) + for _, newBundle := range newBundles { + trustDomains[newBundle.TrustDomain()] = struct{}{} + s.jwtAuthorities[newBundle.TrustDomain()] = newBundle.JWTAuthorities() + } + + // Remove the JWT authority entries for trust domains no longer represented + // in the JWT bundles. + for existingTD := range s.jwtAuthorities { + if _, ok := trustDomains[existingTD]; ok { + continue + } + delete(s.jwtAuthorities, existingTD) + } +} + +func (s *BundleSource) checkClosed() error { + s.closeMtx.RLock() + defer s.closeMtx.RUnlock() + if s.closed { + return bundlesourceErr.New("source is closed") + } + return nil +} diff --git a/examples/spire_producer.example/workloadapi/client.go b/examples/spire_producer.example/workloadapi/client.go new file mode 100644 index 000000000..3328a98fb --- /dev/null +++ b/examples/spire_producer.example/workloadapi/client.go @@ -0,0 +1,549 @@ +package workloadapi + +import ( + "context" + "crypto/x509" + "errors" + "fmt" + "time" + + "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/logger" + "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// Client is a Workload API client. +type Client struct { + conn *grpc.ClientConn + wlClient workload.SpiffeWorkloadAPIClient + config clientConfig +} + +// New dials the Workload API and returns a client. The client should be closed +// when no longer in use to free underlying resources. +func New(ctx context.Context, options ...ClientOption) (*Client, error) { + c := &Client{ + config: defaultClientConfig(), + } + for _, opt := range options { + opt.configureClient(&c.config) + } + + err := c.setAddress() + if err != nil { + return nil, err + } + + c.conn, err = c.newConn(ctx) + if err != nil { + return nil, err + } + + c.wlClient = workload.NewSpiffeWorkloadAPIClient(c.conn) + return c, nil +} + +// Close closes the client. +func (c *Client) Close() error { + return c.conn.Close() +} + +// FetchX509SVID fetches the default X509-SVID, i.e. the first in the list +// returned by the Workload API. +func (c *Client) FetchX509SVID(ctx context.Context) (*x509svid.SVID, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) + if err != nil { + return nil, err + } + + resp, err := stream.Recv() + if err != nil { + return nil, err + } + + svids, err := parseX509SVIDs(resp, true) + if err != nil { + return nil, err + } + + return svids[0], nil +} + +// FetchX509SVIDs fetches all X509-SVIDs. +func (c *Client) FetchX509SVIDs(ctx context.Context) ([]*x509svid.SVID, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) + if err != nil { + return nil, err + } + + resp, err := stream.Recv() + if err != nil { + return nil, err + } + + return parseX509SVIDs(resp, false) +} + +// FetchX509Bundles fetches the X.509 bundles. +func (c *Client) FetchX509Bundles(ctx context.Context) (*x509bundle.Set, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + stream, err := c.wlClient.FetchX509Bundles(ctx, &workload.X509BundlesRequest{}) + if err != nil { + return nil, err + } + resp, err := stream.Recv() + if err != nil { + return nil, err + } + + return parseX509BundlesResponse(resp) +} + +// WatchX509Bundles watches for changes to the X.509 bundles. The watcher receives +// the updated X.509 bundles. +func (c *Client) WatchX509Bundles(ctx context.Context, watcher X509BundleWatcher) error { + backoff := newBackoff() + for { + err := c.watchX509Bundles(ctx, watcher, backoff) + watcher.OnX509BundlesWatchError(err) + err = c.handleWatchError(ctx, err, backoff) + if err != nil { + return err + } + } +} + +// FetchX509Context fetches the X.509 context, which contains both X509-SVIDs +// and X.509 bundles. +func (c *Client) FetchX509Context(ctx context.Context) (*X509Context, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) + if err != nil { + return nil, err + } + + resp, err := stream.Recv() + if err != nil { + return nil, err + } + + return parseX509Context(resp) +} + +// WatchX509Context watches for updates to the X.509 context. The watcher +// receives the updated X.509 context. +func (c *Client) WatchX509Context(ctx context.Context, watcher X509ContextWatcher) error { + backoff := newBackoff() + for { + err := c.watchX509Context(ctx, watcher, backoff) + watcher.OnX509ContextWatchError(err) + err = c.handleWatchError(ctx, err, backoff) + if err != nil { + return err + } + } +} + +// FetchJWTSVID fetches a JWT-SVID. +func (c *Client) FetchJWTSVID(ctx context.Context, params jwtsvid.Params) (*jwtsvid.SVID, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + audience := append([]string{params.Audience}, params.ExtraAudiences...) + resp, err := c.wlClient.FetchJWTSVID(ctx, &workload.JWTSVIDRequest{ + SpiffeId: params.Subject.String(), + Audience: audience, + }) + if err != nil { + return nil, err + } + + svids, err := parseJWTSVIDs(resp, audience, true) + if err != nil { + return nil, err + } + + return svids[0], nil +} + +// FetchJWTSVIDs fetches all JWT-SVIDs. +func (c *Client) FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params) ([]*jwtsvid.SVID, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + audience := append([]string{params.Audience}, params.ExtraAudiences...) + resp, err := c.wlClient.FetchJWTSVID(ctx, &workload.JWTSVIDRequest{ + SpiffeId: params.Subject.String(), + Audience: audience, + }) + if err != nil { + return nil, err + } + + return parseJWTSVIDs(resp, audience, false) +} + +// FetchJWTBundles fetches the JWT bundles for JWT-SVID validation, keyed +// by a SPIFFE ID of the trust domain to which they belong. +func (c *Client) FetchJWTBundles(ctx context.Context) (*jwtbundle.Set, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + stream, err := c.wlClient.FetchJWTBundles(ctx, &workload.JWTBundlesRequest{}) + if err != nil { + return nil, err + } + + resp, err := stream.Recv() + if err != nil { + return nil, err + } + + return parseJWTSVIDBundles(resp) +} + +// WatchJWTBundles watches for changes to the JWT bundles. The watcher receives +// the updated JWT bundles. +func (c *Client) WatchJWTBundles(ctx context.Context, watcher JWTBundleWatcher) error { + backoff := newBackoff() + for { + err := c.watchJWTBundles(ctx, watcher, backoff) + watcher.OnJWTBundlesWatchError(err) + err = c.handleWatchError(ctx, err, backoff) + if err != nil { + return err + } + } +} + +// ValidateJWTSVID validates the JWT-SVID token. The parsed and validated +// JWT-SVID is returned. +func (c *Client) ValidateJWTSVID(ctx context.Context, token, audience string) (*jwtsvid.SVID, error) { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + _, err := c.wlClient.ValidateJWTSVID(ctx, &workload.ValidateJWTSVIDRequest{ + Svid: token, + Audience: audience, + }) + if err != nil { + return nil, err + } + + return jwtsvid.ParseInsecure(token, []string{audience}) +} + +func (c *Client) newConn(ctx context.Context) (*grpc.ClientConn, error) { + c.config.dialOptions = append(c.config.dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + c.appendDialOptionsOS() + return grpc.DialContext(ctx, c.config.address, c.config.dialOptions...) +} + +func (c *Client) handleWatchError(ctx context.Context, err error, backoff *backoff) error { + code := status.Code(err) + if code == codes.Canceled { + return err + } + + if code == codes.InvalidArgument { + c.config.log.Errorf("Canceling watch: %v", err) + return err + } + + c.config.log.Errorf("Failed to watch the Workload API: %v", err) + retryAfter := backoff.Duration() + c.config.log.Debugf("Retrying watch in %s", retryAfter) + select { + case <-time.After(retryAfter): + return nil + + case <-ctx.Done(): + return ctx.Err() + } +} + +func (c *Client) watchX509Context(ctx context.Context, watcher X509ContextWatcher, backoff *backoff) error { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + c.config.log.Debugf("Watching X.509 contexts") + stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) + if err != nil { + return err + } + + for { + resp, err := stream.Recv() + if err != nil { + return err + } + + backoff.Reset() + x509Context, err := parseX509Context(resp) + if err != nil { + c.config.log.Errorf("Failed to parse X509-SVID response: %v", err) + watcher.OnX509ContextWatchError(err) + continue + } + watcher.OnX509ContextUpdate(x509Context) + } +} + +func (c *Client) watchJWTBundles(ctx context.Context, watcher JWTBundleWatcher, backoff *backoff) error { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + c.config.log.Debugf("Watching JWT bundles") + stream, err := c.wlClient.FetchJWTBundles(ctx, &workload.JWTBundlesRequest{}) + if err != nil { + return err + } + + for { + resp, err := stream.Recv() + if err != nil { + return err + } + + backoff.Reset() + jwtbundleSet, err := parseJWTSVIDBundles(resp) + if err != nil { + c.config.log.Errorf("Failed to parse JWT bundle response: %v", err) + watcher.OnJWTBundlesWatchError(err) + continue + } + watcher.OnJWTBundlesUpdate(jwtbundleSet) + } +} + +func (c *Client) watchX509Bundles(ctx context.Context, watcher X509BundleWatcher, backoff *backoff) error { + ctx, cancel := context.WithCancel(withHeader(ctx)) + defer cancel() + + c.config.log.Debugf("Watching X.509 bundles") + stream, err := c.wlClient.FetchX509Bundles(ctx, &workload.X509BundlesRequest{}) + if err != nil { + return err + } + + for { + resp, err := stream.Recv() + if err != nil { + return err + } + + backoff.Reset() + x509bundleSet, err := parseX509BundlesResponse(resp) + if err != nil { + c.config.log.Errorf("Failed to parse X.509 bundle response: %v", err) + watcher.OnX509BundlesWatchError(err) + continue + } + watcher.OnX509BundlesUpdate(x509bundleSet) + } +} + +// X509ContextWatcher receives X509Context updates from the Workload API. +type X509ContextWatcher interface { + // OnX509ContextUpdate is called with the latest X.509 context retrieved + // from the Workload API. + OnX509ContextUpdate(*X509Context) + + // OnX509ContextWatchError is called when there is a problem establishing + // or maintaining connectivity with the Workload API. + OnX509ContextWatchError(error) +} + +// JWTBundleWatcher receives JWT bundle updates from the Workload API. +type JWTBundleWatcher interface { + // OnJWTBundlesUpdate is called with the latest JWT bundle set retrieved + // from the Workload API. + OnJWTBundlesUpdate(*jwtbundle.Set) + + // OnJWTBundlesWatchError is called when there is a problem establishing + // or maintaining connectivity with the Workload API. + OnJWTBundlesWatchError(error) +} + +// X509BundleWatcher receives X.509 bundle updates from the Workload API. +type X509BundleWatcher interface { + // OnX509BundlesUpdate is called with the latest X.509 bundle set retrieved + // from the Workload API. + OnX509BundlesUpdate(*x509bundle.Set) + + // OnX509BundlesWatchError is called when there is a problem establishing + // or maintaining connectivity with the Workload API. + OnX509BundlesWatchError(error) +} + +func withHeader(ctx context.Context) context.Context { + header := metadata.Pairs("workload.spiffe.io", "true") + return metadata.NewOutgoingContext(ctx, header) +} + +func defaultClientConfig() clientConfig { + return clientConfig{ + log: logger.Null, + } +} + +func parseX509Context(resp *workload.X509SVIDResponse) (*X509Context, error) { + svids, err := parseX509SVIDs(resp, false) + if err != nil { + return nil, err + } + + bundles, err := parseX509Bundles(resp) + if err != nil { + return nil, err + } + + return &X509Context{ + SVIDs: svids, + Bundles: bundles, + }, nil +} + +// parseX509SVIDs parses one or all of the SVIDs in the response. If firstOnly +// is true, then only the first SVID in the response is parsed and returned. +// Otherwise all SVIDs are parsed and returned. +func parseX509SVIDs(resp *workload.X509SVIDResponse, firstOnly bool) ([]*x509svid.SVID, error) { + n := len(resp.Svids) + if n == 0 { + return nil, errors.New("no SVIDs in response") + } + if firstOnly { + n = 1 + } + + svids := make([]*x509svid.SVID, 0, n) + for i := 0; i < n; i++ { + svid := resp.Svids[i] + s, err := x509svid.ParseRaw(svid.X509Svid, svid.X509SvidKey) + if err != nil { + return nil, err + } + svids = append(svids, s) + } + + return svids, nil +} + +func parseX509Bundles(resp *workload.X509SVIDResponse) (*x509bundle.Set, error) { + bundles := []*x509bundle.Bundle{} + for _, svid := range resp.Svids { + b, err := parseX509Bundle(svid.SpiffeId, svid.Bundle) + if err != nil { + return nil, err + } + bundles = append(bundles, b) + } + + for tdID, bundle := range resp.FederatedBundles { + b, err := parseX509Bundle(tdID, bundle) + if err != nil { + return nil, err + } + bundles = append(bundles, b) + } + + return x509bundle.NewSet(bundles...), nil +} + +func parseX509Bundle(spiffeID string, bundle []byte) (*x509bundle.Bundle, error) { + td, err := spiffeid.TrustDomainFromString(spiffeID) + if err != nil { + return nil, err + } + certs, err := x509.ParseCertificates(bundle) + if err != nil { + return nil, err + } + if len(certs) == 0 { + return nil, fmt.Errorf("empty X.509 bundle for trust domain %q", td) + } + return x509bundle.FromX509Authorities(td, certs), nil +} + +func parseX509BundlesResponse(resp *workload.X509BundlesResponse) (*x509bundle.Set, error) { + bundles := []*x509bundle.Bundle{} + + for tdID, b := range resp.Bundles { + td, err := spiffeid.TrustDomainFromString(tdID) + if err != nil { + return nil, err + } + + b, err := x509bundle.ParseRaw(td, b) + if err != nil { + return nil, err + } + bundles = append(bundles, b) + } + + return x509bundle.NewSet(bundles...), nil +} + +// parseJWTSVIDs parses one or all of the SVIDs in the response. If firstOnly +// is true, then only the first SVID in the response is parsed and returned. +// Otherwise all SVIDs are parsed and returned. +func parseJWTSVIDs(resp *workload.JWTSVIDResponse, audience []string, firstOnly bool) ([]*jwtsvid.SVID, error) { + n := len(resp.Svids) + if n == 0 { + return nil, errors.New("there were no SVIDs in the response") + } + if firstOnly { + n = 1 + } + + svids := make([]*jwtsvid.SVID, 0, n) + for i := 0; i < n; i++ { + svid := resp.Svids[i] + s, err := jwtsvid.ParseInsecure(svid.Svid, audience) + if err != nil { + return nil, err + } + svids = append(svids, s) + } + + return svids, nil +} + +func parseJWTSVIDBundles(resp *workload.JWTBundlesResponse) (*jwtbundle.Set, error) { + bundles := []*jwtbundle.Bundle{} + + for tdID, b := range resp.Bundles { + td, err := spiffeid.TrustDomainFromString(tdID) + if err != nil { + return nil, err + } + + b, err := jwtbundle.Parse(td, b) + if err != nil { + return nil, err + } + bundles = append(bundles, b) + } + + return jwtbundle.NewSet(bundles...), nil +} diff --git a/examples/spire_producer.example/workloadapi/client_posix.go b/examples/spire_producer.example/workloadapi/client_posix.go new file mode 100644 index 000000000..8e91a28fa --- /dev/null +++ b/examples/spire_producer.example/workloadapi/client_posix.go @@ -0,0 +1,29 @@ +//go:build !windows +// +build !windows + +package workloadapi + +import "errors" + +// appendDialOptionsOS appends OS specific dial options +func (c *Client) appendDialOptionsOS() { + // No options to add in this platform +} +func (c *Client) setAddress() error { + if c.config.namedPipeName != "" { + // Purely defensive. This should never happen. + return errors.New("named pipes not supported in this platform") + } + + if c.config.address == "" { + var ok bool + c.config.address, ok = GetDefaultAddress() + if !ok { + return errors.New("workload endpoint socket address is not configured") + } + } + + var err error + c.config.address, err = parseTargetFromStringAddr(c.config.address) + return err +} diff --git a/examples/spire_producer.example/workloadapi/client_windows.go b/examples/spire_producer.example/workloadapi/client_windows.go new file mode 100644 index 000000000..fb628fccc --- /dev/null +++ b/examples/spire_producer.example/workloadapi/client_windows.go @@ -0,0 +1,57 @@ +//go:build windows +// +build windows + +package workloadapi + +import ( + "errors" + "path/filepath" + "strings" + + "github.com/Microsoft/go-winio" + "google.golang.org/grpc" +) + +// appendDialOptionsOS appends OS specific dial options +func (c *Client) appendDialOptionsOS() { + if c.config.namedPipeName != "" { + // Use the dialer to connect to named pipes only if a named pipe + // is defined (i.e. WithNamedPipeName is used). + c.config.dialOptions = append(c.config.dialOptions, grpc.WithContextDialer(winio.DialPipeContext)) + } +} + +func (c *Client) setAddress() error { + var err error + if c.config.namedPipeName != "" { + if c.config.address != "" { + return errors.New("only one of WithAddr or WithNamedPipeName options can be used, not both") + } + c.config.address = namedPipeTarget(c.config.namedPipeName) + return nil + } + + if c.config.address == "" { + var ok bool + c.config.address, ok = GetDefaultAddress() + if !ok { + return errors.New("workload endpoint socket address is not configured") + } + } + + if strings.HasPrefix(c.config.address, "npipe:") { + // Use the dialer to connect to named pipes only if the gRPC target + // string has the "npipe" scheme + c.config.dialOptions = append(c.config.dialOptions, grpc.WithContextDialer(winio.DialPipeContext)) + } + + c.config.address, err = parseTargetFromStringAddr(c.config.address) + return err +} + +// namedPipeTarget returns a target string suitable for +// dialing the endpoint address based on the provided +// pipe name. +func namedPipeTarget(pipeName string) string { + return `\\.\` + filepath.Join("pipe", pipeName) +} diff --git a/examples/spire_producer.example/workloadapi/convenience.go b/examples/spire_producer.example/workloadapi/convenience.go new file mode 100644 index 000000000..f42c226fa --- /dev/null +++ b/examples/spire_producer.example/workloadapi/convenience.go @@ -0,0 +1,124 @@ +package workloadapi + +import ( + "context" + + "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" +) + +// FetchX509SVID fetches the default X509-SVID, i.e. the first in the list +// returned by the Workload API. +func FetchX509SVID(ctx context.Context, options ...ClientOption) (*x509svid.SVID, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.FetchX509SVID(ctx) +} + +// FetchX509SVIDs fetches all X509-SVIDs. +func FetchX509SVIDs(ctx context.Context, options ...ClientOption) ([]*x509svid.SVID, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.FetchX509SVIDs(ctx) +} + +// FetchX509Bundle fetches the X.509 bundles. +func FetchX509Bundles(ctx context.Context, options ...ClientOption) (*x509bundle.Set, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.FetchX509Bundles(ctx) +} + +// FetchX509Context fetches the X.509 context, which contains both X509-SVIDs +// and X.509 bundles. +func FetchX509Context(ctx context.Context, options ...ClientOption) (*X509Context, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.FetchX509Context(ctx) +} + +// WatchX509Context watches for updates to the X.509 context. +func WatchX509Context(ctx context.Context, watcher X509ContextWatcher, options ...ClientOption) error { + c, err := New(ctx, options...) + if err != nil { + return err + } + defer c.Close() + return c.WatchX509Context(ctx, watcher) +} + +// FetchJWTSVID fetches a JWT-SVID. +func FetchJWTSVID(ctx context.Context, params jwtsvid.Params, options ...ClientOption) (*jwtsvid.SVID, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.FetchJWTSVID(ctx, params) +} + +// FetchJWTSVID fetches all JWT-SVIDs. +func FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params, options ...ClientOption) ([]*jwtsvid.SVID, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.FetchJWTSVIDs(ctx, params) +} + +// FetchJWTBundles fetches the JWT bundles for JWT-SVID validation, keyed +// by a SPIFFE ID of the trust domain to which they belong. +func FetchJWTBundles(ctx context.Context, options ...ClientOption) (*jwtbundle.Set, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.FetchJWTBundles(ctx) +} + +// WatchJWTBundles watches for changes to the JWT bundles. +func WatchJWTBundles(ctx context.Context, watcher JWTBundleWatcher, options ...ClientOption) error { + c, err := New(ctx, options...) + if err != nil { + return err + } + defer c.Close() + return c.WatchJWTBundles(ctx, watcher) +} + +// WatchX509Bundles watches for changes to the X.509 bundles. +func WatchX509Bundles(ctx context.Context, watcher X509BundleWatcher, options ...ClientOption) error { + c, err := New(ctx, options...) + if err != nil { + return err + } + defer c.Close() + return c.WatchX509Bundles(ctx, watcher) +} + +// ValidateJWTSVID validates the JWT-SVID token. The parsed and validated +// JWT-SVID is returned. +func ValidateJWTSVID(ctx context.Context, token, audience string, options ...ClientOption) (*jwtsvid.SVID, error) { + c, err := New(ctx, options...) + if err != nil { + return nil, err + } + defer c.Close() + return c.ValidateJWTSVID(ctx, token, audience) +} diff --git a/examples/spire_producer.example/workloadapi/jwtsource.go b/examples/spire_producer.example/workloadapi/jwtsource.go new file mode 100644 index 000000000..47ea83ade --- /dev/null +++ b/examples/spire_producer.example/workloadapi/jwtsource.go @@ -0,0 +1,109 @@ +package workloadapi + +import ( + "context" + "sync" + + "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/zeebo/errs" +) + +var jwtsourceErr = errs.Class("jwtsource") + +// JWTSource is a source of JWT-SVID and JWT bundles maintained via the +// Workload API. +type JWTSource struct { + watcher *watcher + + mtx sync.RWMutex + bundles *jwtbundle.Set + + closeMtx sync.RWMutex + closed bool +} + +// NewJWTSource creates a new JWTSource. It blocks until the initial update +// has been received from the Workload API. The source should be closed when +// no longer in use to free underlying resources. +func NewJWTSource(ctx context.Context, options ...JWTSourceOption) (_ *JWTSource, err error) { + config := &jwtSourceConfig{} + for _, option := range options { + option.configureJWTSource(config) + } + + s := &JWTSource{} + + s.watcher, err = newWatcher(ctx, config.watcher, nil, s.setJWTBundles) + if err != nil { + return nil, err + } + + return s, nil +} + +// Close closes the source, dropping the connection to the Workload API. +// Other source methods will return an error after Close has been called. +// The underlying Workload API client will also be closed if it is owned by +// the JWTSource (i.e. not provided via the WithClient option). +func (s *JWTSource) Close() error { + s.closeMtx.Lock() + s.closed = true + s.closeMtx.Unlock() + + return s.watcher.Close() +} + +// FetchJWTSVID fetches a JWT-SVID from the source with the given parameters. +// It implements the jwtsvid.Source interface. +func (s *JWTSource) FetchJWTSVID(ctx context.Context, params jwtsvid.Params) (*jwtsvid.SVID, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + return s.watcher.client.FetchJWTSVID(ctx, params) +} + +// FetchJWTSVIDs fetches all JWT-SVIDs from the source with the given parameters. +// It implements the jwtsvid.Source interface. +func (s *JWTSource) FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params) ([]*jwtsvid.SVID, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + return s.watcher.client.FetchJWTSVIDs(ctx, params) +} + +// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust +// domain. It implements the jwtbundle.Source interface. +func (s *JWTSource) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + return s.bundles.GetJWTBundleForTrustDomain(trustDomain) +} + +// WaitUntilUpdated waits until the source is updated or the context is done, +// in which case ctx.Err() is returned. +func (s *JWTSource) WaitUntilUpdated(ctx context.Context) error { + return s.watcher.WaitUntilUpdated(ctx) +} + +// Updated returns a channel that is sent on whenever the source is updated. +func (s *JWTSource) Updated() <-chan struct{} { + return s.watcher.Updated() +} + +func (s *JWTSource) setJWTBundles(bundles *jwtbundle.Set) { + s.mtx.Lock() + defer s.mtx.Unlock() + s.bundles = bundles +} + +func (s *JWTSource) checkClosed() error { + s.closeMtx.RLock() + defer s.closeMtx.RUnlock() + if s.closed { + return jwtsourceErr.New("source is closed") + } + return nil +} diff --git a/examples/spire_producer.example/workloadapi/option.go b/examples/spire_producer.example/workloadapi/option.go new file mode 100644 index 000000000..00cab7d16 --- /dev/null +++ b/examples/spire_producer.example/workloadapi/option.go @@ -0,0 +1,147 @@ +package workloadapi + +import ( + "github.com/spiffe/go-spiffe/v2/logger" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "google.golang.org/grpc" +) + +// ClientOption is an option used when creating a new Client. +type ClientOption interface { + configureClient(*clientConfig) +} + +// WithAddr provides an address for the Workload API. The value of the +// SPIFFE_ENDPOINT_SOCKET environment variable will be used if the option +// is unused. +func WithAddr(addr string) ClientOption { + return clientOption(func(c *clientConfig) { + c.address = addr + }) +} + +// WithDialOptions provides extra GRPC dialing options when dialing the +// Workload API. +func WithDialOptions(options ...grpc.DialOption) ClientOption { + return clientOption(func(c *clientConfig) { + c.dialOptions = append(c.dialOptions, options...) + }) +} + +// WithLogger provides a logger to the Client. +func WithLogger(logger logger.Logger) ClientOption { + return clientOption(func(c *clientConfig) { + c.log = logger + }) +} + +// SourceOption are options that are shared among all option types. +type SourceOption interface { + configureX509Source(*x509SourceConfig) + configureJWTSource(*jwtSourceConfig) + configureBundleSource(*bundleSourceConfig) +} + +// WithClient provides a Client for the source to use. If unset, a new Client +// will be created. +func WithClient(client *Client) SourceOption { + return withClient{client: client} +} + +// WithClientOptions controls the options used to create a new Client for the +// source. This option will be ignored if WithClient is used. +func WithClientOptions(options ...ClientOption) SourceOption { + return withClientOptions{options: options} +} + +// X509SourceOption is an option for the X509Source. A SourceOption is also an +// X509SourceOption. +type X509SourceOption interface { + configureX509Source(*x509SourceConfig) +} + +// WithDefaultX509SVIDPicker provides a function that is used to determine the +// default X509-SVID when more than one is provided by the Workload API. By +// default, the first X509-SVID in the list returned by the Workload API is +// used. +func WithDefaultX509SVIDPicker(picker func([]*x509svid.SVID) *x509svid.SVID) X509SourceOption { + return withDefaultX509SVIDPicker{picker: picker} +} + +// JWTSourceOption is an option for the JWTSource. A SourceOption is also a +// JWTSourceOption. +type JWTSourceOption interface { + configureJWTSource(*jwtSourceConfig) +} + +// BundleSourceOption is an option for the BundleSource. A SourceOption is also +// a BundleSourceOption. +type BundleSourceOption interface { + configureBundleSource(*bundleSourceConfig) +} + +type clientConfig struct { + address string + namedPipeName string + dialOptions []grpc.DialOption + log logger.Logger +} + +type clientOption func(*clientConfig) + +func (fn clientOption) configureClient(config *clientConfig) { + fn(config) +} + +type x509SourceConfig struct { + watcher watcherConfig + picker func([]*x509svid.SVID) *x509svid.SVID +} + +type jwtSourceConfig struct { + watcher watcherConfig +} + +type bundleSourceConfig struct { + watcher watcherConfig +} + +type withClient struct { + client *Client +} + +func (o withClient) configureX509Source(config *x509SourceConfig) { + config.watcher.client = o.client +} + +func (o withClient) configureJWTSource(config *jwtSourceConfig) { + config.watcher.client = o.client +} + +func (o withClient) configureBundleSource(config *bundleSourceConfig) { + config.watcher.client = o.client +} + +type withClientOptions struct { + options []ClientOption +} + +func (o withClientOptions) configureX509Source(config *x509SourceConfig) { + config.watcher.clientOptions = o.options +} + +func (o withClientOptions) configureJWTSource(config *jwtSourceConfig) { + config.watcher.clientOptions = o.options +} + +func (o withClientOptions) configureBundleSource(config *bundleSourceConfig) { + config.watcher.clientOptions = o.options +} + +type withDefaultX509SVIDPicker struct { + picker func([]*x509svid.SVID) *x509svid.SVID +} + +func (o withDefaultX509SVIDPicker) configureX509Source(config *x509SourceConfig) { + config.picker = o.picker +} diff --git a/examples/spire_producer.example/workloadapi/option_windows.go b/examples/spire_producer.example/workloadapi/option_windows.go new file mode 100644 index 000000000..c06e5338f --- /dev/null +++ b/examples/spire_producer.example/workloadapi/option_windows.go @@ -0,0 +1,12 @@ +//go:build windows +// +build windows + +package workloadapi + +// WithNamedPipeName provides a Pipe Name for the Workload API +// endpoint in the form \\.\pipe\. +func WithNamedPipeName(pipeName string) ClientOption { + return clientOption(func(c *clientConfig) { + c.namedPipeName = pipeName + }) +} diff --git a/examples/spire_producer.example/workloadapi/watcher.go b/examples/spire_producer.example/workloadapi/watcher.go new file mode 100644 index 000000000..f110e0738 --- /dev/null +++ b/examples/spire_producer.example/workloadapi/watcher.go @@ -0,0 +1,191 @@ +package workloadapi + +import ( + "context" + "sync" + + "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/zeebo/errs" +) + +type sourceClient interface { + WatchX509Context(context.Context, X509ContextWatcher) error + WatchJWTBundles(context.Context, JWTBundleWatcher) error + FetchJWTSVID(context.Context, jwtsvid.Params) (*jwtsvid.SVID, error) + FetchJWTSVIDs(context.Context, jwtsvid.Params) ([]*jwtsvid.SVID, error) + Close() error +} + +type watcherConfig struct { + client sourceClient + clientOptions []ClientOption +} + +type watcher struct { + updatedCh chan struct{} + + client sourceClient + ownsClient bool + + cancel func() + wg sync.WaitGroup + + closeMtx sync.Mutex + closed bool + closeErr error + + x509ContextFn func(*X509Context) + x509ContextSet chan struct{} + x509ContextSetOnce sync.Once + + jwtBundlesFn func(*jwtbundle.Set) + jwtBundlesSet chan struct{} + jwtBundlesSetOnce sync.Once +} + +func newWatcher(ctx context.Context, config watcherConfig, x509ContextFn func(*X509Context), jwtBundlesFn func(*jwtbundle.Set)) (_ *watcher, err error) { + w := &watcher{ + updatedCh: make(chan struct{}, 1), + client: config.client, + cancel: func() {}, + x509ContextFn: x509ContextFn, + x509ContextSet: make(chan struct{}), + jwtBundlesFn: jwtBundlesFn, + jwtBundlesSet: make(chan struct{}), + } + + // If this function fails, we need to clean up the source. + defer func() { + if err != nil { + err = errs.Combine(err, w.Close()) + } + }() + + // Initialize a new client unless one is provided by the options + if w.client == nil { + client, err := New(ctx, config.clientOptions...) + if err != nil { + return nil, err + } + w.client = client + w.ownsClient = true + } + + errCh := make(chan error, 2) + waitFor := func(has <-chan struct{}) error { + select { + case <-has: + return nil + case err := <-errCh: + return err + case <-ctx.Done(): + return ctx.Err() + } + } + + // Kick up a background goroutine that watches the Workload API for + // updates. + var watchCtx context.Context + watchCtx, w.cancel = context.WithCancel(context.Background()) + + if w.x509ContextFn != nil { + w.wg.Add(1) + go func() { + defer w.wg.Done() + errCh <- w.client.WatchX509Context(watchCtx, w) + }() + if err := waitFor(w.x509ContextSet); err != nil { + return nil, err + } + } + + if w.jwtBundlesFn != nil { + w.wg.Add(1) + go func() { + defer w.wg.Done() + errCh <- w.client.WatchJWTBundles(watchCtx, w) + }() + if err := waitFor(w.jwtBundlesSet); err != nil { + return nil, err + } + } + + // Drain the update channel since this function blocks until an update and + // don't want callers to think there was an update on the source right + // after it was initialized. If we ever allow the watcher to be initialzed + // without waiting, this reset should be removed. + w.drainUpdated() + + return w, nil +} + +// Close closes the watcher, dropping the connection to the Workload API. +func (w *watcher) Close() error { + w.closeMtx.Lock() + defer w.closeMtx.Unlock() + + if !w.closed { + w.cancel() + w.wg.Wait() + + // Close() can be called by New() to close a partially intialized source. + // Only close the client if it has been set and the source owns it. + if w.client != nil && w.ownsClient { + w.closeErr = w.client.Close() + } + w.closed = true + } + return w.closeErr +} + +func (w *watcher) OnX509ContextUpdate(x509Context *X509Context) { + w.x509ContextFn(x509Context) + w.x509ContextSetOnce.Do(func() { + close(w.x509ContextSet) + }) + w.triggerUpdated() +} + +func (w *watcher) OnX509ContextWatchError(err error) { + // The watcher doesn't do anything special with the error. If logging is + // desired, it should be provided to the Workload API client. +} + +func (w *watcher) OnJWTBundlesUpdate(jwtBundles *jwtbundle.Set) { + w.jwtBundlesFn(jwtBundles) + w.jwtBundlesSetOnce.Do(func() { + close(w.jwtBundlesSet) + }) + w.triggerUpdated() +} + +func (w *watcher) OnJWTBundlesWatchError(error) { + // The watcher doesn't do anything special with the error. If logging is + // desired, it should be provided to the Workload API client. +} + +func (w *watcher) WaitUntilUpdated(ctx context.Context) error { + select { + case <-w.updatedCh: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (w *watcher) Updated() <-chan struct{} { + return w.updatedCh +} + +func (w *watcher) drainUpdated() { + select { + case <-w.updatedCh: + default: + } +} + +func (w *watcher) triggerUpdated() { + w.drainUpdated() + w.updatedCh <- struct{}{} +} diff --git a/examples/spire_producer.example/workloadapi/x509context.go b/examples/spire_producer.example/workloadapi/x509context.go new file mode 100644 index 000000000..94a9392b4 --- /dev/null +++ b/examples/spire_producer.example/workloadapi/x509context.go @@ -0,0 +1,23 @@ +package workloadapi + +import ( + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" +) + +// X509Context conveys X.509 materials from the Workload API. +type X509Context struct { + // SVIDs is a list of workload X509-SVIDs. + SVIDs []*x509svid.SVID + + // Bundles is a set of X.509 bundles. + Bundles *x509bundle.Set +} + +// Default returns the default X509-SVID (the first in the list). +// +// See the SPIFFE Workload API standard Section 5.3. +// (https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#53-default-identity) +func (x *X509Context) DefaultSVID() *x509svid.SVID { + return x.SVIDs[0] +} diff --git a/examples/spire_producer.example/workloadapi/x509source.go b/examples/spire_producer.example/workloadapi/x509source.go new file mode 100644 index 000000000..28287f68e --- /dev/null +++ b/examples/spire_producer.example/workloadapi/x509source.go @@ -0,0 +1,124 @@ +package workloadapi + +import ( + "context" + "sync" + + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "github.com/zeebo/errs" +) + +var x509sourceErr = errs.Class("x509source") + +// X509Source is a source of X509-SVIDs and X.509 bundles maintained via the +// Workload API. +type X509Source struct { + watcher *watcher + picker func([]*x509svid.SVID) *x509svid.SVID + + mtx sync.RWMutex + svid *x509svid.SVID + bundles *x509bundle.Set + + closeMtx sync.RWMutex + closed bool +} + +// NewX509Source creates a new X509Source. It blocks until the initial update +// has been received from the Workload API. The source should be closed when +// no longer in use to free underlying resources. +func NewX509Source(ctx context.Context, options ...X509SourceOption) (_ *X509Source, err error) { + config := &x509SourceConfig{} + for _, option := range options { + option.configureX509Source(config) + } + + s := &X509Source{ + picker: config.picker, + } + + s.watcher, err = newWatcher(ctx, config.watcher, s.setX509Context, nil) + if err != nil { + return nil, err + } + + return s, nil +} + +// Close closes the source, dropping the connection to the Workload API. +// Other source methods will return an error after Close has been called. +// The underlying Workload API client will also be closed if it is owned by +// the X509Source (i.e. not provided via the WithClient option). +func (s *X509Source) Close() (err error) { + s.closeMtx.Lock() + s.closed = true + s.closeMtx.Unlock() + + return s.watcher.Close() +} + +// GetX509SVID returns an X509-SVID from the source. It implements the +// x509svid.Source interface. +func (s *X509Source) GetX509SVID() (*x509svid.SVID, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + + s.mtx.RLock() + svid := s.svid + s.mtx.RUnlock() + + if svid == nil { + // This is a defensive check and should be unreachable since the source + // waits for the initial Workload API update before returning from + // New(). + return nil, x509sourceErr.New("missing X509-SVID") + } + return svid, nil +} + +// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust +// domain. It implements the x509bundle.Source interface. +func (s *X509Source) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + if err := s.checkClosed(); err != nil { + return nil, err + } + + return s.bundles.GetX509BundleForTrustDomain(trustDomain) +} + +// WaitUntilUpdated waits until the source is updated or the context is done, +// in which case ctx.Err() is returned. +func (s *X509Source) WaitUntilUpdated(ctx context.Context) error { + return s.watcher.WaitUntilUpdated(ctx) +} + +// Updated returns a channel that is sent on whenever the source is updated. +func (s *X509Source) Updated() <-chan struct{} { + return s.watcher.Updated() +} + +func (s *X509Source) setX509Context(x509Context *X509Context) { + var svid *x509svid.SVID + if s.picker == nil { + svid = x509Context.DefaultSVID() + } else { + svid = s.picker(x509Context.SVIDs) + } + + s.mtx.Lock() + defer s.mtx.Unlock() + s.svid = svid + s.bundles = x509Context.Bundles +} + +func (s *X509Source) checkClosed() error { + s.closeMtx.RLock() + defer s.closeMtx.RUnlock() + if s.closed { + return x509sourceErr.New("source is closed") + } + return nil +} From 1fdc6b0aaa6233b58530d75e3b87a865bbe55dfa Mon Sep 17 00:00:00 2001 From: Chang You Date: Fri, 23 Jun 2023 14:12:52 -0700 Subject: [PATCH 02/11] Updated closer error and changed oauthConf to principal --- .../spire_producer.example.go | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go index d7265870d..93b2ed273 100644 --- a/examples/spire_producer.example/spire_producer.example.go +++ b/examples/spire_producer.example/spire_producer.example.go @@ -42,7 +42,8 @@ type tokenAuth struct { // previously-received token is 80% of the way to its expiration time). func handleJWTTokenRefreshEvent(ctx context.Context, client kafka.Handle, principal, socketPath string, audience []string) { fmt.Fprintf(os.Stderr, "Token refresh\n") - oauthBearerToken, retrieveErr := retrieveJWTToken(ctx, principal, socketPath, audience) + oauthBearerToken, closer, retrieveErr := retrieveJWTToken(ctx, principal, socketPath, audience) + defer closer() if retrieveErr != nil { fmt.Fprintf(os.Stderr, "%% Token retrieval error: %v\n", retrieveErr) client.SetOAuthBearerTokenFailure(retrieveErr.Error()) @@ -55,13 +56,13 @@ func handleJWTTokenRefreshEvent(ctx context.Context, client kafka.Handle, princi } } -func retrieveJWTToken(ctx context.Context, principal, socketPath string, audience []string) (kafka.OAuthBearerToken, error) { +func retrieveJWTToken(ctx context.Context, principal, socketPath string, audience []string) (kafka.OAuthBearerToken, func() error, error) { jwtSource, err := workloadapi.NewJWTSource( ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), ) if err != nil { - return kafka.OAuthBearerToken{}, fmt.Errorf("unable to create JWTSource: %w", err) + return kafka.OAuthBearerToken{}, nil, fmt.Errorf("unable to create JWTSource: %w", err) } defer jwtSource.Close() @@ -74,7 +75,7 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc jwtSVID, err := jwtSource.FetchJWTSVID(ctx, params) if err != nil { - return kafka.OAuthBearerToken{}, fmt.Errorf("unable to fetch JWT SVID: %w", err) + return kafka.OAuthBearerToken{}, nil, fmt.Errorf("unable to fetch JWT SVID: %w", err) } oauthBearerToken := kafka.OAuthBearerToken{ @@ -84,7 +85,7 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc Extensions: map[string]string{}, } - return oauthBearerToken, nil + return oauthBearerToken, jwtSource.Close, nil } func main() { @@ -98,17 +99,15 @@ func main() { topic := os.Args[2] principal := os.Args[3] socketPath := os.Args[4] - audience := []string{"audience1", "audience2"} // Audience should be defined properly + audience := []string{"audience1", "audience2"} // You'll probably need to modify this configuration to // match your environment. config := kafka.ConfigMap{ - "bootstrap.servers": bootstrapServers, - "security.protocol": "SASL_PLAINTEXT", - "sasl.mechanisms": "OAUTHBEARER", - "sasl.oauthbearer.config": map[string]string{ - "principal": principal, - }, + "bootstrap.servers": bootstrapServers, + "security.protocol": "SASL_PLAINTEXT", + "sasl.mechanisms": "OAUTHBEARER", + "sasl.oauthbearer.config": principal, } p, err := kafka.NewProducer(&config) From d1f1fbd71fd1df95a961d8e4ed937486f8a344c0 Mon Sep 17 00:00:00 2001 From: Chang You Date: Fri, 30 Jun 2023 15:15:38 -0700 Subject: [PATCH 03/11] Modified config to use the defualt unsecure method, pass logicalCluster and identityPollId as extentions to oauthBearerToken --- examples/spire_producer.example/spire_producer.example.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go index 93b2ed273..a7fce9d95 100644 --- a/examples/spire_producer.example/spire_producer.example.go +++ b/examples/spire_producer.example/spire_producer.example.go @@ -78,11 +78,15 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc return kafka.OAuthBearerToken{}, nil, fmt.Errorf("unable to fetch JWT SVID: %w", err) } + extensions := map[string]string{ + "logicalCluster": "lkc-r6gdo0", + "identityPoolId": "pool-W9j5", + } oauthBearerToken := kafka.OAuthBearerToken{ TokenValue: jwtSVID.Marshal(), Expiration: jwtSVID.Expiry, Principal: principal, - Extensions: map[string]string{}, + Extensions: extensions, } return oauthBearerToken, jwtSource.Close, nil @@ -105,7 +109,7 @@ func main() { // match your environment. config := kafka.ConfigMap{ "bootstrap.servers": bootstrapServers, - "security.protocol": "SASL_PLAINTEXT", + "security.protocol": "SASL_SSL", "sasl.mechanisms": "OAUTHBEARER", "sasl.oauthbearer.config": principal, } From 87ba39f1e0bf23420b0d62e22b72281ac9f02bd5 Mon Sep 17 00:00:00 2001 From: Chang You Date: Fri, 7 Jul 2023 14:44:36 -0700 Subject: [PATCH 04/11] Deleted imported workload API, clean the code --- .../spire_producer.example.go | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go index a7fce9d95..d7ca351f4 100644 --- a/examples/spire_producer.example/spire_producer.example.go +++ b/examples/spire_producer.example/spire_producer.example.go @@ -7,7 +7,6 @@ import ( "github.com/spiffe/go-spiffe/v2/workloadapi" "os" "os/signal" - "regexp" "syscall" "time" @@ -16,25 +15,6 @@ import ( _ "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" ) -var ( - // Regex for sasl.oauthbearer.config, which constrains it to be - // 1 or more name=value pairs with optional ignored whitespace - oauthbearerConfigRegex = regexp.MustCompile("^(\\s*(\\w+)\\s*=\\s*(\\w+))+\\s*$") - // Regex used to extract name=value pairs from sasl.oauthbearer.config - oauthbearerNameEqualsValueRegex = regexp.MustCompile("(\\w+)\\s*=\\s*(\\w+)") -) - -const ( - principalClaimNameKey = "principalClaimName" - principalKey = "principal" - joseHeaderEncoded = "eyJhbGciOiJub25lIn0" // {"alg":"none"} -) - -type tokenAuth struct { - audience []string - tokenSource *workloadapi.JWTSource -} - // handleJWTTokenRefreshEvent retrieves JWT from the SPIRE workload API and // sets the token on the client for use in any future authentication attempt. // It must be invoked whenever kafka.OAuthBearerTokenRefresh appears on the client's event channel, @@ -61,6 +41,7 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), ) + if err != nil { return kafka.OAuthBearerToken{}, nil, fmt.Errorf("unable to create JWTSource: %w", err) } @@ -79,7 +60,7 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc } extensions := map[string]string{ - "logicalCluster": "lkc-r6gdo0", + "logicalCluster": "lkc-vk3y7z", "identityPoolId": "pool-W9j5", } oauthBearerToken := kafka.OAuthBearerToken{ From 1f69a9e5cddd032310a342867e092d16d49ba763 Mon Sep 17 00:00:00 2001 From: Chang You Date: Fri, 7 Jul 2023 15:04:50 -0700 Subject: [PATCH 05/11] Deleted Spire workload API --- .../workloadapi/addr.go | 69 --- .../workloadapi/addr_posix.go | 34 -- .../workloadapi/addr_windows.go | 33 -- .../workloadapi/backoff.go | 34 -- .../workloadapi/bundlesource.go | 188 ------ .../workloadapi/client.go | 549 ------------------ .../workloadapi/client_posix.go | 29 - .../workloadapi/client_windows.go | 57 -- .../workloadapi/convenience.go | 124 ---- .../workloadapi/jwtsource.go | 109 ---- .../workloadapi/option.go | 147 ----- .../workloadapi/option_windows.go | 12 - .../workloadapi/watcher.go | 191 ------ .../workloadapi/x509context.go | 23 - .../workloadapi/x509source.go | 124 ---- 15 files changed, 1723 deletions(-) delete mode 100644 examples/spire_producer.example/workloadapi/addr.go delete mode 100644 examples/spire_producer.example/workloadapi/addr_posix.go delete mode 100644 examples/spire_producer.example/workloadapi/addr_windows.go delete mode 100644 examples/spire_producer.example/workloadapi/backoff.go delete mode 100644 examples/spire_producer.example/workloadapi/bundlesource.go delete mode 100644 examples/spire_producer.example/workloadapi/client.go delete mode 100644 examples/spire_producer.example/workloadapi/client_posix.go delete mode 100644 examples/spire_producer.example/workloadapi/client_windows.go delete mode 100644 examples/spire_producer.example/workloadapi/convenience.go delete mode 100644 examples/spire_producer.example/workloadapi/jwtsource.go delete mode 100644 examples/spire_producer.example/workloadapi/option.go delete mode 100644 examples/spire_producer.example/workloadapi/option_windows.go delete mode 100644 examples/spire_producer.example/workloadapi/watcher.go delete mode 100644 examples/spire_producer.example/workloadapi/x509context.go delete mode 100644 examples/spire_producer.example/workloadapi/x509source.go diff --git a/examples/spire_producer.example/workloadapi/addr.go b/examples/spire_producer.example/workloadapi/addr.go deleted file mode 100644 index 6ce0238fe..000000000 --- a/examples/spire_producer.example/workloadapi/addr.go +++ /dev/null @@ -1,69 +0,0 @@ -package workloadapi - -import ( - "errors" - "net" - "net/url" - "os" -) - -const ( - // SocketEnv is the environment variable holding the default Workload API - // address. - SocketEnv = "SPIFFE_ENDPOINT_SOCKET" -) - -func GetDefaultAddress() (string, bool) { - return os.LookupEnv(SocketEnv) -} - -// ValidateAddress validates that the provided address -// can be parsed to a gRPC target string for dialing -// a Workload API endpoint exposed as either a Unix -// Domain Socket or TCP socket. -func ValidateAddress(addr string) error { - _, err := parseTargetFromStringAddr(addr) - return err -} - -// parseTargetFromStringAddr parses the endpoint address and returns a gRPC target -// string for dialing. -func parseTargetFromStringAddr(addr string) (string, error) { - u, err := url.Parse(addr) - if err != nil { - return "", errors.New("workload endpoint socket is not a valid URI: " + err.Error()) - } - return parseTargetFromURLAddr(u) -} - -func parseTargetFromURLAddr(u *url.URL) (string, error) { - if u.Scheme == "tcp" { - switch { - case u.Opaque != "": - return "", errors.New("workload endpoint tcp socket URI must not be opaque") - case u.User != nil: - return "", errors.New("workload endpoint tcp socket URI must not include user info") - case u.Host == "": - return "", errors.New("workload endpoint tcp socket URI must include a host") - case u.Path != "": - return "", errors.New("workload endpoint tcp socket URI must not include a path") - case u.RawQuery != "": - return "", errors.New("workload endpoint tcp socket URI must not include query values") - case u.Fragment != "": - return "", errors.New("workload endpoint tcp socket URI must not include a fragment") - } - - ip := net.ParseIP(u.Hostname()) - if ip == nil { - return "", errors.New("workload endpoint tcp socket URI host component must be an IP:port") - } - port := u.Port() - if port == "" { - return "", errors.New("workload endpoint tcp socket URI host component must include a port") - } - - return net.JoinHostPort(ip.String(), port), nil - } - - return parseTargetFromURLAddrOS(u) -} diff --git a/examples/spire_producer.example/workloadapi/addr_posix.go b/examples/spire_producer.example/workloadapi/addr_posix.go deleted file mode 100644 index 0fa3f56b7..000000000 --- a/examples/spire_producer.example/workloadapi/addr_posix.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build !windows -// +build !windows - -package workloadapi - -import ( - "errors" - "net/url" -) - -var ( - ErrInvalidEndpointScheme = errors.New("workload endpoint socket URI must have a \"tcp\" or \"unix\" scheme") -) - -func parseTargetFromURLAddrOS(u *url.URL) (string, error) { - switch u.Scheme { - case "unix": - switch { - case u.Opaque != "": - return "", errors.New("workload endpoint unix socket URI must not be opaque") - case u.User != nil: - return "", errors.New("workload endpoint unix socket URI must not include user info") - case u.Host == "" && u.Path == "": - return "", errors.New("workload endpoint unix socket URI must include a path") - case u.RawQuery != "": - return "", errors.New("workload endpoint unix socket URI must not include query values") - case u.Fragment != "": - return "", errors.New("workload endpoint unix socket URI must not include a fragment") - } - return u.String(), nil - default: - return "", ErrInvalidEndpointScheme - } -} diff --git a/examples/spire_producer.example/workloadapi/addr_windows.go b/examples/spire_producer.example/workloadapi/addr_windows.go deleted file mode 100644 index 4f9f2c352..000000000 --- a/examples/spire_producer.example/workloadapi/addr_windows.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build windows -// +build windows - -package workloadapi - -import ( - "errors" - "net/url" -) - -var ( - ErrInvalidEndpointScheme = errors.New("workload endpoint socket URI must have a \"tcp\" or \"npipe\" scheme") -) - -func parseTargetFromURLAddrOS(u *url.URL) (string, error) { - switch u.Scheme { - case "npipe": - switch { - case u.Opaque == "" && u.Host != "": - return "", errors.New("workload endpoint named pipe URI must be opaque") - case u.Opaque == "": - return "", errors.New("workload endpoint named pipe URI must include an opaque part") - case u.RawQuery != "": - return "", errors.New("workload endpoint named pipe URI must not include query values") - case u.Fragment != "": - return "", errors.New("workload endpoint named pipe URI must not include a fragment") - } - - return namedPipeTarget(u.Opaque), nil - default: - return "", ErrInvalidEndpointScheme - } -} diff --git a/examples/spire_producer.example/workloadapi/backoff.go b/examples/spire_producer.example/workloadapi/backoff.go deleted file mode 100644 index b6ef1ed53..000000000 --- a/examples/spire_producer.example/workloadapi/backoff.go +++ /dev/null @@ -1,34 +0,0 @@ -package workloadapi - -import ( - "math" - "time" -) - -// backoff defines an linear backoff policy. -type backoff struct { - InitialDelay time.Duration - MaxDelay time.Duration - n int -} - -func newBackoff() *backoff { - return &backoff{ - InitialDelay: time.Second, - MaxDelay: 30 * time.Second, - n: 0, - } -} - -// Duration returns the next wait period for the backoff. Not goroutine-safe. -func (b *backoff) Duration() time.Duration { - backoff := float64(b.n) + 1 - d := math.Min(b.InitialDelay.Seconds()*backoff, b.MaxDelay.Seconds()) - b.n++ - return time.Duration(d) * time.Second -} - -// Reset resets the backoff's state. -func (b *backoff) Reset() { - b.n = 0 -} diff --git a/examples/spire_producer.example/workloadapi/bundlesource.go b/examples/spire_producer.example/workloadapi/bundlesource.go deleted file mode 100644 index 2a253efc7..000000000 --- a/examples/spire_producer.example/workloadapi/bundlesource.go +++ /dev/null @@ -1,188 +0,0 @@ -package workloadapi - -import ( - "context" - "crypto" - "crypto/x509" - "sync" - - "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" - "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" - "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" - "github.com/spiffe/go-spiffe/v2/spiffeid" - "github.com/zeebo/errs" -) - -var bundlesourceErr = errs.Class("bundlesource") - -// BundleSource is a source of SPIFFE bundles maintained via the Workload API. -type BundleSource struct { - watcher *watcher - - mtx sync.RWMutex - x509Authorities map[spiffeid.TrustDomain][]*x509.Certificate - jwtAuthorities map[spiffeid.TrustDomain]map[string]crypto.PublicKey - - closeMtx sync.RWMutex - closed bool -} - -// NewBundleSource creates a new BundleSource. It blocks until the initial -// update has been received from the Workload API. The source should be closed -// when no longer in use to free underlying resources. -func NewBundleSource(ctx context.Context, options ...BundleSourceOption) (_ *BundleSource, err error) { - config := &bundleSourceConfig{} - for _, option := range options { - option.configureBundleSource(config) - } - - s := &BundleSource{ - x509Authorities: make(map[spiffeid.TrustDomain][]*x509.Certificate), - jwtAuthorities: make(map[spiffeid.TrustDomain]map[string]crypto.PublicKey), - } - - s.watcher, err = newWatcher(ctx, config.watcher, s.setX509Context, s.setJWTBundles) - if err != nil { - return nil, err - } - - return s, nil -} - -// Close closes the source, dropping the connection to the Workload API. -// Other source methods will return an error after Close has been called. -// The underlying Workload API client will also be closed if it is owned by -// the BundleSource (i.e. not provided via the WithClient option). -func (s *BundleSource) Close() error { - s.closeMtx.Lock() - s.closed = true - s.closeMtx.Unlock() - - return s.watcher.Close() -} - -// GetBundleForTrustDomain returns the SPIFFE bundle for the given trust -// domain. It implements the spiffebundle.Source interface. -func (s *BundleSource) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*spiffebundle.Bundle, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - s.mtx.RLock() - defer s.mtx.RUnlock() - - x509Authorities, hasX509Authorities := s.x509Authorities[trustDomain] - jwtAuthorities, hasJWTAuthorities := s.jwtAuthorities[trustDomain] - if !hasX509Authorities && !hasJWTAuthorities { - return nil, bundlesourceErr.New("no SPIFFE bundle for trust domain %q", trustDomain) - } - bundle := spiffebundle.New(trustDomain) - if hasX509Authorities { - bundle.SetX509Authorities(x509Authorities) - } - if hasJWTAuthorities { - bundle.SetJWTAuthorities(jwtAuthorities) - } - return bundle, nil -} - -// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust -// domain. It implements the x509bundle.Source interface. -func (s *BundleSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - s.mtx.RLock() - defer s.mtx.RUnlock() - - x509Authorities, hasX509Authorities := s.x509Authorities[trustDomain] - if !hasX509Authorities { - return nil, bundlesourceErr.New("no X.509 bundle for trust domain %q", trustDomain) - } - return x509bundle.FromX509Authorities(trustDomain, x509Authorities), nil -} - -// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust -// domain. It implements the jwtbundle.Source interface. -func (s *BundleSource) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - s.mtx.RLock() - defer s.mtx.RUnlock() - - jwtAuthorities, hasJWTAuthorities := s.jwtAuthorities[trustDomain] - if !hasJWTAuthorities { - return nil, bundlesourceErr.New("no JWT bundle for trust domain %q", trustDomain) - } - return jwtbundle.FromJWTAuthorities(trustDomain, jwtAuthorities), nil -} - -// WaitUntilUpdated waits until the source is updated or the context is done, -// in which case ctx.Err() is returned. -func (s *BundleSource) WaitUntilUpdated(ctx context.Context) error { - return s.watcher.WaitUntilUpdated(ctx) -} - -// Updated returns a channel that is sent on whenever the source is updated. -func (s *BundleSource) Updated() <-chan struct{} { - return s.watcher.Updated() -} - -func (s *BundleSource) setX509Context(x509Context *X509Context) { - s.mtx.Lock() - defer s.mtx.Unlock() - - newBundles := x509Context.Bundles.Bundles() - - // Add/replace the X.509 authorities from the X.509 context. Track the trust - // domains represented in the new X.509 context so we can determine which - // existing trust domains are no longer represented. - trustDomains := make(map[spiffeid.TrustDomain]struct{}, len(newBundles)) - for _, newBundle := range newBundles { - trustDomains[newBundle.TrustDomain()] = struct{}{} - s.x509Authorities[newBundle.TrustDomain()] = newBundle.X509Authorities() - } - - // Remove the X.509 authority entries for trust domains no longer - // represented in the X.509 context. - for existingTD := range s.x509Authorities { - if _, ok := trustDomains[existingTD]; ok { - continue - } - delete(s.x509Authorities, existingTD) - } -} - -func (s *BundleSource) setJWTBundles(bundles *jwtbundle.Set) { - s.mtx.Lock() - defer s.mtx.Unlock() - - newBundles := bundles.Bundles() - - // Add/replace the JWT authorities from the JWT bundles. Track the trust - // domains represented in the new JWT bundles so we can determine which - // existing trust domains are no longer represented. - trustDomains := make(map[spiffeid.TrustDomain]struct{}, len(newBundles)) - for _, newBundle := range newBundles { - trustDomains[newBundle.TrustDomain()] = struct{}{} - s.jwtAuthorities[newBundle.TrustDomain()] = newBundle.JWTAuthorities() - } - - // Remove the JWT authority entries for trust domains no longer represented - // in the JWT bundles. - for existingTD := range s.jwtAuthorities { - if _, ok := trustDomains[existingTD]; ok { - continue - } - delete(s.jwtAuthorities, existingTD) - } -} - -func (s *BundleSource) checkClosed() error { - s.closeMtx.RLock() - defer s.closeMtx.RUnlock() - if s.closed { - return bundlesourceErr.New("source is closed") - } - return nil -} diff --git a/examples/spire_producer.example/workloadapi/client.go b/examples/spire_producer.example/workloadapi/client.go deleted file mode 100644 index 3328a98fb..000000000 --- a/examples/spire_producer.example/workloadapi/client.go +++ /dev/null @@ -1,549 +0,0 @@ -package workloadapi - -import ( - "context" - "crypto/x509" - "errors" - "fmt" - "time" - - "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" - "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" - "github.com/spiffe/go-spiffe/v2/logger" - "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" - "github.com/spiffe/go-spiffe/v2/spiffeid" - "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" - "github.com/spiffe/go-spiffe/v2/svid/x509svid" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -// Client is a Workload API client. -type Client struct { - conn *grpc.ClientConn - wlClient workload.SpiffeWorkloadAPIClient - config clientConfig -} - -// New dials the Workload API and returns a client. The client should be closed -// when no longer in use to free underlying resources. -func New(ctx context.Context, options ...ClientOption) (*Client, error) { - c := &Client{ - config: defaultClientConfig(), - } - for _, opt := range options { - opt.configureClient(&c.config) - } - - err := c.setAddress() - if err != nil { - return nil, err - } - - c.conn, err = c.newConn(ctx) - if err != nil { - return nil, err - } - - c.wlClient = workload.NewSpiffeWorkloadAPIClient(c.conn) - return c, nil -} - -// Close closes the client. -func (c *Client) Close() error { - return c.conn.Close() -} - -// FetchX509SVID fetches the default X509-SVID, i.e. the first in the list -// returned by the Workload API. -func (c *Client) FetchX509SVID(ctx context.Context) (*x509svid.SVID, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) - if err != nil { - return nil, err - } - - resp, err := stream.Recv() - if err != nil { - return nil, err - } - - svids, err := parseX509SVIDs(resp, true) - if err != nil { - return nil, err - } - - return svids[0], nil -} - -// FetchX509SVIDs fetches all X509-SVIDs. -func (c *Client) FetchX509SVIDs(ctx context.Context) ([]*x509svid.SVID, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) - if err != nil { - return nil, err - } - - resp, err := stream.Recv() - if err != nil { - return nil, err - } - - return parseX509SVIDs(resp, false) -} - -// FetchX509Bundles fetches the X.509 bundles. -func (c *Client) FetchX509Bundles(ctx context.Context) (*x509bundle.Set, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - stream, err := c.wlClient.FetchX509Bundles(ctx, &workload.X509BundlesRequest{}) - if err != nil { - return nil, err - } - resp, err := stream.Recv() - if err != nil { - return nil, err - } - - return parseX509BundlesResponse(resp) -} - -// WatchX509Bundles watches for changes to the X.509 bundles. The watcher receives -// the updated X.509 bundles. -func (c *Client) WatchX509Bundles(ctx context.Context, watcher X509BundleWatcher) error { - backoff := newBackoff() - for { - err := c.watchX509Bundles(ctx, watcher, backoff) - watcher.OnX509BundlesWatchError(err) - err = c.handleWatchError(ctx, err, backoff) - if err != nil { - return err - } - } -} - -// FetchX509Context fetches the X.509 context, which contains both X509-SVIDs -// and X.509 bundles. -func (c *Client) FetchX509Context(ctx context.Context) (*X509Context, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) - if err != nil { - return nil, err - } - - resp, err := stream.Recv() - if err != nil { - return nil, err - } - - return parseX509Context(resp) -} - -// WatchX509Context watches for updates to the X.509 context. The watcher -// receives the updated X.509 context. -func (c *Client) WatchX509Context(ctx context.Context, watcher X509ContextWatcher) error { - backoff := newBackoff() - for { - err := c.watchX509Context(ctx, watcher, backoff) - watcher.OnX509ContextWatchError(err) - err = c.handleWatchError(ctx, err, backoff) - if err != nil { - return err - } - } -} - -// FetchJWTSVID fetches a JWT-SVID. -func (c *Client) FetchJWTSVID(ctx context.Context, params jwtsvid.Params) (*jwtsvid.SVID, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - audience := append([]string{params.Audience}, params.ExtraAudiences...) - resp, err := c.wlClient.FetchJWTSVID(ctx, &workload.JWTSVIDRequest{ - SpiffeId: params.Subject.String(), - Audience: audience, - }) - if err != nil { - return nil, err - } - - svids, err := parseJWTSVIDs(resp, audience, true) - if err != nil { - return nil, err - } - - return svids[0], nil -} - -// FetchJWTSVIDs fetches all JWT-SVIDs. -func (c *Client) FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params) ([]*jwtsvid.SVID, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - audience := append([]string{params.Audience}, params.ExtraAudiences...) - resp, err := c.wlClient.FetchJWTSVID(ctx, &workload.JWTSVIDRequest{ - SpiffeId: params.Subject.String(), - Audience: audience, - }) - if err != nil { - return nil, err - } - - return parseJWTSVIDs(resp, audience, false) -} - -// FetchJWTBundles fetches the JWT bundles for JWT-SVID validation, keyed -// by a SPIFFE ID of the trust domain to which they belong. -func (c *Client) FetchJWTBundles(ctx context.Context) (*jwtbundle.Set, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - stream, err := c.wlClient.FetchJWTBundles(ctx, &workload.JWTBundlesRequest{}) - if err != nil { - return nil, err - } - - resp, err := stream.Recv() - if err != nil { - return nil, err - } - - return parseJWTSVIDBundles(resp) -} - -// WatchJWTBundles watches for changes to the JWT bundles. The watcher receives -// the updated JWT bundles. -func (c *Client) WatchJWTBundles(ctx context.Context, watcher JWTBundleWatcher) error { - backoff := newBackoff() - for { - err := c.watchJWTBundles(ctx, watcher, backoff) - watcher.OnJWTBundlesWatchError(err) - err = c.handleWatchError(ctx, err, backoff) - if err != nil { - return err - } - } -} - -// ValidateJWTSVID validates the JWT-SVID token. The parsed and validated -// JWT-SVID is returned. -func (c *Client) ValidateJWTSVID(ctx context.Context, token, audience string) (*jwtsvid.SVID, error) { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - _, err := c.wlClient.ValidateJWTSVID(ctx, &workload.ValidateJWTSVIDRequest{ - Svid: token, - Audience: audience, - }) - if err != nil { - return nil, err - } - - return jwtsvid.ParseInsecure(token, []string{audience}) -} - -func (c *Client) newConn(ctx context.Context) (*grpc.ClientConn, error) { - c.config.dialOptions = append(c.config.dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) - c.appendDialOptionsOS() - return grpc.DialContext(ctx, c.config.address, c.config.dialOptions...) -} - -func (c *Client) handleWatchError(ctx context.Context, err error, backoff *backoff) error { - code := status.Code(err) - if code == codes.Canceled { - return err - } - - if code == codes.InvalidArgument { - c.config.log.Errorf("Canceling watch: %v", err) - return err - } - - c.config.log.Errorf("Failed to watch the Workload API: %v", err) - retryAfter := backoff.Duration() - c.config.log.Debugf("Retrying watch in %s", retryAfter) - select { - case <-time.After(retryAfter): - return nil - - case <-ctx.Done(): - return ctx.Err() - } -} - -func (c *Client) watchX509Context(ctx context.Context, watcher X509ContextWatcher, backoff *backoff) error { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - c.config.log.Debugf("Watching X.509 contexts") - stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) - if err != nil { - return err - } - - for { - resp, err := stream.Recv() - if err != nil { - return err - } - - backoff.Reset() - x509Context, err := parseX509Context(resp) - if err != nil { - c.config.log.Errorf("Failed to parse X509-SVID response: %v", err) - watcher.OnX509ContextWatchError(err) - continue - } - watcher.OnX509ContextUpdate(x509Context) - } -} - -func (c *Client) watchJWTBundles(ctx context.Context, watcher JWTBundleWatcher, backoff *backoff) error { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - c.config.log.Debugf("Watching JWT bundles") - stream, err := c.wlClient.FetchJWTBundles(ctx, &workload.JWTBundlesRequest{}) - if err != nil { - return err - } - - for { - resp, err := stream.Recv() - if err != nil { - return err - } - - backoff.Reset() - jwtbundleSet, err := parseJWTSVIDBundles(resp) - if err != nil { - c.config.log.Errorf("Failed to parse JWT bundle response: %v", err) - watcher.OnJWTBundlesWatchError(err) - continue - } - watcher.OnJWTBundlesUpdate(jwtbundleSet) - } -} - -func (c *Client) watchX509Bundles(ctx context.Context, watcher X509BundleWatcher, backoff *backoff) error { - ctx, cancel := context.WithCancel(withHeader(ctx)) - defer cancel() - - c.config.log.Debugf("Watching X.509 bundles") - stream, err := c.wlClient.FetchX509Bundles(ctx, &workload.X509BundlesRequest{}) - if err != nil { - return err - } - - for { - resp, err := stream.Recv() - if err != nil { - return err - } - - backoff.Reset() - x509bundleSet, err := parseX509BundlesResponse(resp) - if err != nil { - c.config.log.Errorf("Failed to parse X.509 bundle response: %v", err) - watcher.OnX509BundlesWatchError(err) - continue - } - watcher.OnX509BundlesUpdate(x509bundleSet) - } -} - -// X509ContextWatcher receives X509Context updates from the Workload API. -type X509ContextWatcher interface { - // OnX509ContextUpdate is called with the latest X.509 context retrieved - // from the Workload API. - OnX509ContextUpdate(*X509Context) - - // OnX509ContextWatchError is called when there is a problem establishing - // or maintaining connectivity with the Workload API. - OnX509ContextWatchError(error) -} - -// JWTBundleWatcher receives JWT bundle updates from the Workload API. -type JWTBundleWatcher interface { - // OnJWTBundlesUpdate is called with the latest JWT bundle set retrieved - // from the Workload API. - OnJWTBundlesUpdate(*jwtbundle.Set) - - // OnJWTBundlesWatchError is called when there is a problem establishing - // or maintaining connectivity with the Workload API. - OnJWTBundlesWatchError(error) -} - -// X509BundleWatcher receives X.509 bundle updates from the Workload API. -type X509BundleWatcher interface { - // OnX509BundlesUpdate is called with the latest X.509 bundle set retrieved - // from the Workload API. - OnX509BundlesUpdate(*x509bundle.Set) - - // OnX509BundlesWatchError is called when there is a problem establishing - // or maintaining connectivity with the Workload API. - OnX509BundlesWatchError(error) -} - -func withHeader(ctx context.Context) context.Context { - header := metadata.Pairs("workload.spiffe.io", "true") - return metadata.NewOutgoingContext(ctx, header) -} - -func defaultClientConfig() clientConfig { - return clientConfig{ - log: logger.Null, - } -} - -func parseX509Context(resp *workload.X509SVIDResponse) (*X509Context, error) { - svids, err := parseX509SVIDs(resp, false) - if err != nil { - return nil, err - } - - bundles, err := parseX509Bundles(resp) - if err != nil { - return nil, err - } - - return &X509Context{ - SVIDs: svids, - Bundles: bundles, - }, nil -} - -// parseX509SVIDs parses one or all of the SVIDs in the response. If firstOnly -// is true, then only the first SVID in the response is parsed and returned. -// Otherwise all SVIDs are parsed and returned. -func parseX509SVIDs(resp *workload.X509SVIDResponse, firstOnly bool) ([]*x509svid.SVID, error) { - n := len(resp.Svids) - if n == 0 { - return nil, errors.New("no SVIDs in response") - } - if firstOnly { - n = 1 - } - - svids := make([]*x509svid.SVID, 0, n) - for i := 0; i < n; i++ { - svid := resp.Svids[i] - s, err := x509svid.ParseRaw(svid.X509Svid, svid.X509SvidKey) - if err != nil { - return nil, err - } - svids = append(svids, s) - } - - return svids, nil -} - -func parseX509Bundles(resp *workload.X509SVIDResponse) (*x509bundle.Set, error) { - bundles := []*x509bundle.Bundle{} - for _, svid := range resp.Svids { - b, err := parseX509Bundle(svid.SpiffeId, svid.Bundle) - if err != nil { - return nil, err - } - bundles = append(bundles, b) - } - - for tdID, bundle := range resp.FederatedBundles { - b, err := parseX509Bundle(tdID, bundle) - if err != nil { - return nil, err - } - bundles = append(bundles, b) - } - - return x509bundle.NewSet(bundles...), nil -} - -func parseX509Bundle(spiffeID string, bundle []byte) (*x509bundle.Bundle, error) { - td, err := spiffeid.TrustDomainFromString(spiffeID) - if err != nil { - return nil, err - } - certs, err := x509.ParseCertificates(bundle) - if err != nil { - return nil, err - } - if len(certs) == 0 { - return nil, fmt.Errorf("empty X.509 bundle for trust domain %q", td) - } - return x509bundle.FromX509Authorities(td, certs), nil -} - -func parseX509BundlesResponse(resp *workload.X509BundlesResponse) (*x509bundle.Set, error) { - bundles := []*x509bundle.Bundle{} - - for tdID, b := range resp.Bundles { - td, err := spiffeid.TrustDomainFromString(tdID) - if err != nil { - return nil, err - } - - b, err := x509bundle.ParseRaw(td, b) - if err != nil { - return nil, err - } - bundles = append(bundles, b) - } - - return x509bundle.NewSet(bundles...), nil -} - -// parseJWTSVIDs parses one or all of the SVIDs in the response. If firstOnly -// is true, then only the first SVID in the response is parsed and returned. -// Otherwise all SVIDs are parsed and returned. -func parseJWTSVIDs(resp *workload.JWTSVIDResponse, audience []string, firstOnly bool) ([]*jwtsvid.SVID, error) { - n := len(resp.Svids) - if n == 0 { - return nil, errors.New("there were no SVIDs in the response") - } - if firstOnly { - n = 1 - } - - svids := make([]*jwtsvid.SVID, 0, n) - for i := 0; i < n; i++ { - svid := resp.Svids[i] - s, err := jwtsvid.ParseInsecure(svid.Svid, audience) - if err != nil { - return nil, err - } - svids = append(svids, s) - } - - return svids, nil -} - -func parseJWTSVIDBundles(resp *workload.JWTBundlesResponse) (*jwtbundle.Set, error) { - bundles := []*jwtbundle.Bundle{} - - for tdID, b := range resp.Bundles { - td, err := spiffeid.TrustDomainFromString(tdID) - if err != nil { - return nil, err - } - - b, err := jwtbundle.Parse(td, b) - if err != nil { - return nil, err - } - bundles = append(bundles, b) - } - - return jwtbundle.NewSet(bundles...), nil -} diff --git a/examples/spire_producer.example/workloadapi/client_posix.go b/examples/spire_producer.example/workloadapi/client_posix.go deleted file mode 100644 index 8e91a28fa..000000000 --- a/examples/spire_producer.example/workloadapi/client_posix.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build !windows -// +build !windows - -package workloadapi - -import "errors" - -// appendDialOptionsOS appends OS specific dial options -func (c *Client) appendDialOptionsOS() { - // No options to add in this platform -} -func (c *Client) setAddress() error { - if c.config.namedPipeName != "" { - // Purely defensive. This should never happen. - return errors.New("named pipes not supported in this platform") - } - - if c.config.address == "" { - var ok bool - c.config.address, ok = GetDefaultAddress() - if !ok { - return errors.New("workload endpoint socket address is not configured") - } - } - - var err error - c.config.address, err = parseTargetFromStringAddr(c.config.address) - return err -} diff --git a/examples/spire_producer.example/workloadapi/client_windows.go b/examples/spire_producer.example/workloadapi/client_windows.go deleted file mode 100644 index fb628fccc..000000000 --- a/examples/spire_producer.example/workloadapi/client_windows.go +++ /dev/null @@ -1,57 +0,0 @@ -//go:build windows -// +build windows - -package workloadapi - -import ( - "errors" - "path/filepath" - "strings" - - "github.com/Microsoft/go-winio" - "google.golang.org/grpc" -) - -// appendDialOptionsOS appends OS specific dial options -func (c *Client) appendDialOptionsOS() { - if c.config.namedPipeName != "" { - // Use the dialer to connect to named pipes only if a named pipe - // is defined (i.e. WithNamedPipeName is used). - c.config.dialOptions = append(c.config.dialOptions, grpc.WithContextDialer(winio.DialPipeContext)) - } -} - -func (c *Client) setAddress() error { - var err error - if c.config.namedPipeName != "" { - if c.config.address != "" { - return errors.New("only one of WithAddr or WithNamedPipeName options can be used, not both") - } - c.config.address = namedPipeTarget(c.config.namedPipeName) - return nil - } - - if c.config.address == "" { - var ok bool - c.config.address, ok = GetDefaultAddress() - if !ok { - return errors.New("workload endpoint socket address is not configured") - } - } - - if strings.HasPrefix(c.config.address, "npipe:") { - // Use the dialer to connect to named pipes only if the gRPC target - // string has the "npipe" scheme - c.config.dialOptions = append(c.config.dialOptions, grpc.WithContextDialer(winio.DialPipeContext)) - } - - c.config.address, err = parseTargetFromStringAddr(c.config.address) - return err -} - -// namedPipeTarget returns a target string suitable for -// dialing the endpoint address based on the provided -// pipe name. -func namedPipeTarget(pipeName string) string { - return `\\.\` + filepath.Join("pipe", pipeName) -} diff --git a/examples/spire_producer.example/workloadapi/convenience.go b/examples/spire_producer.example/workloadapi/convenience.go deleted file mode 100644 index f42c226fa..000000000 --- a/examples/spire_producer.example/workloadapi/convenience.go +++ /dev/null @@ -1,124 +0,0 @@ -package workloadapi - -import ( - "context" - - "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" - "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" - "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" - "github.com/spiffe/go-spiffe/v2/svid/x509svid" -) - -// FetchX509SVID fetches the default X509-SVID, i.e. the first in the list -// returned by the Workload API. -func FetchX509SVID(ctx context.Context, options ...ClientOption) (*x509svid.SVID, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.FetchX509SVID(ctx) -} - -// FetchX509SVIDs fetches all X509-SVIDs. -func FetchX509SVIDs(ctx context.Context, options ...ClientOption) ([]*x509svid.SVID, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.FetchX509SVIDs(ctx) -} - -// FetchX509Bundle fetches the X.509 bundles. -func FetchX509Bundles(ctx context.Context, options ...ClientOption) (*x509bundle.Set, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.FetchX509Bundles(ctx) -} - -// FetchX509Context fetches the X.509 context, which contains both X509-SVIDs -// and X.509 bundles. -func FetchX509Context(ctx context.Context, options ...ClientOption) (*X509Context, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.FetchX509Context(ctx) -} - -// WatchX509Context watches for updates to the X.509 context. -func WatchX509Context(ctx context.Context, watcher X509ContextWatcher, options ...ClientOption) error { - c, err := New(ctx, options...) - if err != nil { - return err - } - defer c.Close() - return c.WatchX509Context(ctx, watcher) -} - -// FetchJWTSVID fetches a JWT-SVID. -func FetchJWTSVID(ctx context.Context, params jwtsvid.Params, options ...ClientOption) (*jwtsvid.SVID, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.FetchJWTSVID(ctx, params) -} - -// FetchJWTSVID fetches all JWT-SVIDs. -func FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params, options ...ClientOption) ([]*jwtsvid.SVID, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.FetchJWTSVIDs(ctx, params) -} - -// FetchJWTBundles fetches the JWT bundles for JWT-SVID validation, keyed -// by a SPIFFE ID of the trust domain to which they belong. -func FetchJWTBundles(ctx context.Context, options ...ClientOption) (*jwtbundle.Set, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.FetchJWTBundles(ctx) -} - -// WatchJWTBundles watches for changes to the JWT bundles. -func WatchJWTBundles(ctx context.Context, watcher JWTBundleWatcher, options ...ClientOption) error { - c, err := New(ctx, options...) - if err != nil { - return err - } - defer c.Close() - return c.WatchJWTBundles(ctx, watcher) -} - -// WatchX509Bundles watches for changes to the X.509 bundles. -func WatchX509Bundles(ctx context.Context, watcher X509BundleWatcher, options ...ClientOption) error { - c, err := New(ctx, options...) - if err != nil { - return err - } - defer c.Close() - return c.WatchX509Bundles(ctx, watcher) -} - -// ValidateJWTSVID validates the JWT-SVID token. The parsed and validated -// JWT-SVID is returned. -func ValidateJWTSVID(ctx context.Context, token, audience string, options ...ClientOption) (*jwtsvid.SVID, error) { - c, err := New(ctx, options...) - if err != nil { - return nil, err - } - defer c.Close() - return c.ValidateJWTSVID(ctx, token, audience) -} diff --git a/examples/spire_producer.example/workloadapi/jwtsource.go b/examples/spire_producer.example/workloadapi/jwtsource.go deleted file mode 100644 index 47ea83ade..000000000 --- a/examples/spire_producer.example/workloadapi/jwtsource.go +++ /dev/null @@ -1,109 +0,0 @@ -package workloadapi - -import ( - "context" - "sync" - - "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" - "github.com/spiffe/go-spiffe/v2/spiffeid" - "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" - "github.com/zeebo/errs" -) - -var jwtsourceErr = errs.Class("jwtsource") - -// JWTSource is a source of JWT-SVID and JWT bundles maintained via the -// Workload API. -type JWTSource struct { - watcher *watcher - - mtx sync.RWMutex - bundles *jwtbundle.Set - - closeMtx sync.RWMutex - closed bool -} - -// NewJWTSource creates a new JWTSource. It blocks until the initial update -// has been received from the Workload API. The source should be closed when -// no longer in use to free underlying resources. -func NewJWTSource(ctx context.Context, options ...JWTSourceOption) (_ *JWTSource, err error) { - config := &jwtSourceConfig{} - for _, option := range options { - option.configureJWTSource(config) - } - - s := &JWTSource{} - - s.watcher, err = newWatcher(ctx, config.watcher, nil, s.setJWTBundles) - if err != nil { - return nil, err - } - - return s, nil -} - -// Close closes the source, dropping the connection to the Workload API. -// Other source methods will return an error after Close has been called. -// The underlying Workload API client will also be closed if it is owned by -// the JWTSource (i.e. not provided via the WithClient option). -func (s *JWTSource) Close() error { - s.closeMtx.Lock() - s.closed = true - s.closeMtx.Unlock() - - return s.watcher.Close() -} - -// FetchJWTSVID fetches a JWT-SVID from the source with the given parameters. -// It implements the jwtsvid.Source interface. -func (s *JWTSource) FetchJWTSVID(ctx context.Context, params jwtsvid.Params) (*jwtsvid.SVID, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - return s.watcher.client.FetchJWTSVID(ctx, params) -} - -// FetchJWTSVIDs fetches all JWT-SVIDs from the source with the given parameters. -// It implements the jwtsvid.Source interface. -func (s *JWTSource) FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params) ([]*jwtsvid.SVID, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - return s.watcher.client.FetchJWTSVIDs(ctx, params) -} - -// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust -// domain. It implements the jwtbundle.Source interface. -func (s *JWTSource) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - return s.bundles.GetJWTBundleForTrustDomain(trustDomain) -} - -// WaitUntilUpdated waits until the source is updated or the context is done, -// in which case ctx.Err() is returned. -func (s *JWTSource) WaitUntilUpdated(ctx context.Context) error { - return s.watcher.WaitUntilUpdated(ctx) -} - -// Updated returns a channel that is sent on whenever the source is updated. -func (s *JWTSource) Updated() <-chan struct{} { - return s.watcher.Updated() -} - -func (s *JWTSource) setJWTBundles(bundles *jwtbundle.Set) { - s.mtx.Lock() - defer s.mtx.Unlock() - s.bundles = bundles -} - -func (s *JWTSource) checkClosed() error { - s.closeMtx.RLock() - defer s.closeMtx.RUnlock() - if s.closed { - return jwtsourceErr.New("source is closed") - } - return nil -} diff --git a/examples/spire_producer.example/workloadapi/option.go b/examples/spire_producer.example/workloadapi/option.go deleted file mode 100644 index 00cab7d16..000000000 --- a/examples/spire_producer.example/workloadapi/option.go +++ /dev/null @@ -1,147 +0,0 @@ -package workloadapi - -import ( - "github.com/spiffe/go-spiffe/v2/logger" - "github.com/spiffe/go-spiffe/v2/svid/x509svid" - "google.golang.org/grpc" -) - -// ClientOption is an option used when creating a new Client. -type ClientOption interface { - configureClient(*clientConfig) -} - -// WithAddr provides an address for the Workload API. The value of the -// SPIFFE_ENDPOINT_SOCKET environment variable will be used if the option -// is unused. -func WithAddr(addr string) ClientOption { - return clientOption(func(c *clientConfig) { - c.address = addr - }) -} - -// WithDialOptions provides extra GRPC dialing options when dialing the -// Workload API. -func WithDialOptions(options ...grpc.DialOption) ClientOption { - return clientOption(func(c *clientConfig) { - c.dialOptions = append(c.dialOptions, options...) - }) -} - -// WithLogger provides a logger to the Client. -func WithLogger(logger logger.Logger) ClientOption { - return clientOption(func(c *clientConfig) { - c.log = logger - }) -} - -// SourceOption are options that are shared among all option types. -type SourceOption interface { - configureX509Source(*x509SourceConfig) - configureJWTSource(*jwtSourceConfig) - configureBundleSource(*bundleSourceConfig) -} - -// WithClient provides a Client for the source to use. If unset, a new Client -// will be created. -func WithClient(client *Client) SourceOption { - return withClient{client: client} -} - -// WithClientOptions controls the options used to create a new Client for the -// source. This option will be ignored if WithClient is used. -func WithClientOptions(options ...ClientOption) SourceOption { - return withClientOptions{options: options} -} - -// X509SourceOption is an option for the X509Source. A SourceOption is also an -// X509SourceOption. -type X509SourceOption interface { - configureX509Source(*x509SourceConfig) -} - -// WithDefaultX509SVIDPicker provides a function that is used to determine the -// default X509-SVID when more than one is provided by the Workload API. By -// default, the first X509-SVID in the list returned by the Workload API is -// used. -func WithDefaultX509SVIDPicker(picker func([]*x509svid.SVID) *x509svid.SVID) X509SourceOption { - return withDefaultX509SVIDPicker{picker: picker} -} - -// JWTSourceOption is an option for the JWTSource. A SourceOption is also a -// JWTSourceOption. -type JWTSourceOption interface { - configureJWTSource(*jwtSourceConfig) -} - -// BundleSourceOption is an option for the BundleSource. A SourceOption is also -// a BundleSourceOption. -type BundleSourceOption interface { - configureBundleSource(*bundleSourceConfig) -} - -type clientConfig struct { - address string - namedPipeName string - dialOptions []grpc.DialOption - log logger.Logger -} - -type clientOption func(*clientConfig) - -func (fn clientOption) configureClient(config *clientConfig) { - fn(config) -} - -type x509SourceConfig struct { - watcher watcherConfig - picker func([]*x509svid.SVID) *x509svid.SVID -} - -type jwtSourceConfig struct { - watcher watcherConfig -} - -type bundleSourceConfig struct { - watcher watcherConfig -} - -type withClient struct { - client *Client -} - -func (o withClient) configureX509Source(config *x509SourceConfig) { - config.watcher.client = o.client -} - -func (o withClient) configureJWTSource(config *jwtSourceConfig) { - config.watcher.client = o.client -} - -func (o withClient) configureBundleSource(config *bundleSourceConfig) { - config.watcher.client = o.client -} - -type withClientOptions struct { - options []ClientOption -} - -func (o withClientOptions) configureX509Source(config *x509SourceConfig) { - config.watcher.clientOptions = o.options -} - -func (o withClientOptions) configureJWTSource(config *jwtSourceConfig) { - config.watcher.clientOptions = o.options -} - -func (o withClientOptions) configureBundleSource(config *bundleSourceConfig) { - config.watcher.clientOptions = o.options -} - -type withDefaultX509SVIDPicker struct { - picker func([]*x509svid.SVID) *x509svid.SVID -} - -func (o withDefaultX509SVIDPicker) configureX509Source(config *x509SourceConfig) { - config.picker = o.picker -} diff --git a/examples/spire_producer.example/workloadapi/option_windows.go b/examples/spire_producer.example/workloadapi/option_windows.go deleted file mode 100644 index c06e5338f..000000000 --- a/examples/spire_producer.example/workloadapi/option_windows.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build windows -// +build windows - -package workloadapi - -// WithNamedPipeName provides a Pipe Name for the Workload API -// endpoint in the form \\.\pipe\. -func WithNamedPipeName(pipeName string) ClientOption { - return clientOption(func(c *clientConfig) { - c.namedPipeName = pipeName - }) -} diff --git a/examples/spire_producer.example/workloadapi/watcher.go b/examples/spire_producer.example/workloadapi/watcher.go deleted file mode 100644 index f110e0738..000000000 --- a/examples/spire_producer.example/workloadapi/watcher.go +++ /dev/null @@ -1,191 +0,0 @@ -package workloadapi - -import ( - "context" - "sync" - - "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" - "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" - "github.com/zeebo/errs" -) - -type sourceClient interface { - WatchX509Context(context.Context, X509ContextWatcher) error - WatchJWTBundles(context.Context, JWTBundleWatcher) error - FetchJWTSVID(context.Context, jwtsvid.Params) (*jwtsvid.SVID, error) - FetchJWTSVIDs(context.Context, jwtsvid.Params) ([]*jwtsvid.SVID, error) - Close() error -} - -type watcherConfig struct { - client sourceClient - clientOptions []ClientOption -} - -type watcher struct { - updatedCh chan struct{} - - client sourceClient - ownsClient bool - - cancel func() - wg sync.WaitGroup - - closeMtx sync.Mutex - closed bool - closeErr error - - x509ContextFn func(*X509Context) - x509ContextSet chan struct{} - x509ContextSetOnce sync.Once - - jwtBundlesFn func(*jwtbundle.Set) - jwtBundlesSet chan struct{} - jwtBundlesSetOnce sync.Once -} - -func newWatcher(ctx context.Context, config watcherConfig, x509ContextFn func(*X509Context), jwtBundlesFn func(*jwtbundle.Set)) (_ *watcher, err error) { - w := &watcher{ - updatedCh: make(chan struct{}, 1), - client: config.client, - cancel: func() {}, - x509ContextFn: x509ContextFn, - x509ContextSet: make(chan struct{}), - jwtBundlesFn: jwtBundlesFn, - jwtBundlesSet: make(chan struct{}), - } - - // If this function fails, we need to clean up the source. - defer func() { - if err != nil { - err = errs.Combine(err, w.Close()) - } - }() - - // Initialize a new client unless one is provided by the options - if w.client == nil { - client, err := New(ctx, config.clientOptions...) - if err != nil { - return nil, err - } - w.client = client - w.ownsClient = true - } - - errCh := make(chan error, 2) - waitFor := func(has <-chan struct{}) error { - select { - case <-has: - return nil - case err := <-errCh: - return err - case <-ctx.Done(): - return ctx.Err() - } - } - - // Kick up a background goroutine that watches the Workload API for - // updates. - var watchCtx context.Context - watchCtx, w.cancel = context.WithCancel(context.Background()) - - if w.x509ContextFn != nil { - w.wg.Add(1) - go func() { - defer w.wg.Done() - errCh <- w.client.WatchX509Context(watchCtx, w) - }() - if err := waitFor(w.x509ContextSet); err != nil { - return nil, err - } - } - - if w.jwtBundlesFn != nil { - w.wg.Add(1) - go func() { - defer w.wg.Done() - errCh <- w.client.WatchJWTBundles(watchCtx, w) - }() - if err := waitFor(w.jwtBundlesSet); err != nil { - return nil, err - } - } - - // Drain the update channel since this function blocks until an update and - // don't want callers to think there was an update on the source right - // after it was initialized. If we ever allow the watcher to be initialzed - // without waiting, this reset should be removed. - w.drainUpdated() - - return w, nil -} - -// Close closes the watcher, dropping the connection to the Workload API. -func (w *watcher) Close() error { - w.closeMtx.Lock() - defer w.closeMtx.Unlock() - - if !w.closed { - w.cancel() - w.wg.Wait() - - // Close() can be called by New() to close a partially intialized source. - // Only close the client if it has been set and the source owns it. - if w.client != nil && w.ownsClient { - w.closeErr = w.client.Close() - } - w.closed = true - } - return w.closeErr -} - -func (w *watcher) OnX509ContextUpdate(x509Context *X509Context) { - w.x509ContextFn(x509Context) - w.x509ContextSetOnce.Do(func() { - close(w.x509ContextSet) - }) - w.triggerUpdated() -} - -func (w *watcher) OnX509ContextWatchError(err error) { - // The watcher doesn't do anything special with the error. If logging is - // desired, it should be provided to the Workload API client. -} - -func (w *watcher) OnJWTBundlesUpdate(jwtBundles *jwtbundle.Set) { - w.jwtBundlesFn(jwtBundles) - w.jwtBundlesSetOnce.Do(func() { - close(w.jwtBundlesSet) - }) - w.triggerUpdated() -} - -func (w *watcher) OnJWTBundlesWatchError(error) { - // The watcher doesn't do anything special with the error. If logging is - // desired, it should be provided to the Workload API client. -} - -func (w *watcher) WaitUntilUpdated(ctx context.Context) error { - select { - case <-w.updatedCh: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -func (w *watcher) Updated() <-chan struct{} { - return w.updatedCh -} - -func (w *watcher) drainUpdated() { - select { - case <-w.updatedCh: - default: - } -} - -func (w *watcher) triggerUpdated() { - w.drainUpdated() - w.updatedCh <- struct{}{} -} diff --git a/examples/spire_producer.example/workloadapi/x509context.go b/examples/spire_producer.example/workloadapi/x509context.go deleted file mode 100644 index 94a9392b4..000000000 --- a/examples/spire_producer.example/workloadapi/x509context.go +++ /dev/null @@ -1,23 +0,0 @@ -package workloadapi - -import ( - "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" - "github.com/spiffe/go-spiffe/v2/svid/x509svid" -) - -// X509Context conveys X.509 materials from the Workload API. -type X509Context struct { - // SVIDs is a list of workload X509-SVIDs. - SVIDs []*x509svid.SVID - - // Bundles is a set of X.509 bundles. - Bundles *x509bundle.Set -} - -// Default returns the default X509-SVID (the first in the list). -// -// See the SPIFFE Workload API standard Section 5.3. -// (https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#53-default-identity) -func (x *X509Context) DefaultSVID() *x509svid.SVID { - return x.SVIDs[0] -} diff --git a/examples/spire_producer.example/workloadapi/x509source.go b/examples/spire_producer.example/workloadapi/x509source.go deleted file mode 100644 index 28287f68e..000000000 --- a/examples/spire_producer.example/workloadapi/x509source.go +++ /dev/null @@ -1,124 +0,0 @@ -package workloadapi - -import ( - "context" - "sync" - - "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" - "github.com/spiffe/go-spiffe/v2/spiffeid" - "github.com/spiffe/go-spiffe/v2/svid/x509svid" - "github.com/zeebo/errs" -) - -var x509sourceErr = errs.Class("x509source") - -// X509Source is a source of X509-SVIDs and X.509 bundles maintained via the -// Workload API. -type X509Source struct { - watcher *watcher - picker func([]*x509svid.SVID) *x509svid.SVID - - mtx sync.RWMutex - svid *x509svid.SVID - bundles *x509bundle.Set - - closeMtx sync.RWMutex - closed bool -} - -// NewX509Source creates a new X509Source. It blocks until the initial update -// has been received from the Workload API. The source should be closed when -// no longer in use to free underlying resources. -func NewX509Source(ctx context.Context, options ...X509SourceOption) (_ *X509Source, err error) { - config := &x509SourceConfig{} - for _, option := range options { - option.configureX509Source(config) - } - - s := &X509Source{ - picker: config.picker, - } - - s.watcher, err = newWatcher(ctx, config.watcher, s.setX509Context, nil) - if err != nil { - return nil, err - } - - return s, nil -} - -// Close closes the source, dropping the connection to the Workload API. -// Other source methods will return an error after Close has been called. -// The underlying Workload API client will also be closed if it is owned by -// the X509Source (i.e. not provided via the WithClient option). -func (s *X509Source) Close() (err error) { - s.closeMtx.Lock() - s.closed = true - s.closeMtx.Unlock() - - return s.watcher.Close() -} - -// GetX509SVID returns an X509-SVID from the source. It implements the -// x509svid.Source interface. -func (s *X509Source) GetX509SVID() (*x509svid.SVID, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - - s.mtx.RLock() - svid := s.svid - s.mtx.RUnlock() - - if svid == nil { - // This is a defensive check and should be unreachable since the source - // waits for the initial Workload API update before returning from - // New(). - return nil, x509sourceErr.New("missing X509-SVID") - } - return svid, nil -} - -// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust -// domain. It implements the x509bundle.Source interface. -func (s *X509Source) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { - if err := s.checkClosed(); err != nil { - return nil, err - } - - return s.bundles.GetX509BundleForTrustDomain(trustDomain) -} - -// WaitUntilUpdated waits until the source is updated or the context is done, -// in which case ctx.Err() is returned. -func (s *X509Source) WaitUntilUpdated(ctx context.Context) error { - return s.watcher.WaitUntilUpdated(ctx) -} - -// Updated returns a channel that is sent on whenever the source is updated. -func (s *X509Source) Updated() <-chan struct{} { - return s.watcher.Updated() -} - -func (s *X509Source) setX509Context(x509Context *X509Context) { - var svid *x509svid.SVID - if s.picker == nil { - svid = x509Context.DefaultSVID() - } else { - svid = s.picker(x509Context.SVIDs) - } - - s.mtx.Lock() - defer s.mtx.Unlock() - s.svid = svid - s.bundles = x509Context.Bundles -} - -func (s *X509Source) checkClosed() error { - s.closeMtx.RLock() - defer s.closeMtx.RUnlock() - if s.closed { - return x509sourceErr.New("source is closed") - } - return nil -} From 9e3286f3351c05dc97de3e675e695363509b18d7 Mon Sep 17 00:00:00 2001 From: Chang You Date: Fri, 7 Jul 2023 17:00:24 -0700 Subject: [PATCH 06/11] Updated go.mod --- examples/go.mod | 1 + examples/go.sum | 18 ++++++++++++++++++ .../spire_producer.example.go | 2 -- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/examples/go.mod b/examples/go.mod index 1d1e84e5c..77e36b04e 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -10,5 +10,6 @@ require ( github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/confluentinc/confluent-kafka-go/v2 v2.2.0-RC1 github.com/gdamore/tcell v1.4.0 + github.com/spiffe/go-spiffe/v2 v2.1.6 // indirect google.golang.org/protobuf v1.30.0 ) diff --git a/examples/go.sum b/examples/go.sum index 0aaf7eb65..3db96927d 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -617,6 +617,8 @@ github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOp github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -951,6 +953,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -1488,6 +1492,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spiffe/go-spiffe/v2 v2.1.6 h1:4SdizuQieFyL9eNU+SPiCArH4kynzaKOOj0VvM8R7Xo= +github.com/spiffe/go-spiffe/v2 v2.1.6/go.mod h1:eVDqm9xFvyqao6C+eQensb9ZPkyNEeaUbqbBpOhBnNk= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1551,6 +1557,8 @@ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= +github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -1615,6 +1623,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1625,6 +1634,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1680,6 +1691,7 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1754,6 +1766,7 @@ golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -1933,6 +1946,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= @@ -1944,6 +1958,7 @@ golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= @@ -2049,6 +2064,7 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2310,10 +2326,12 @@ google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go index d7ca351f4..c1c6f2702 100644 --- a/examples/spire_producer.example/spire_producer.example.go +++ b/examples/spire_producer.example/spire_producer.example.go @@ -11,8 +11,6 @@ import ( "time" "github.com/confluentinc/confluent-kafka-go/v2/kafka" - _ "github.com/spiffe/go-spiffe/v2/spiffeid" - _ "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" ) // handleJWTTokenRefreshEvent retrieves JWT from the SPIRE workload API and From ff33ee20b473595550c3e6a0a80124bceef6a83f Mon Sep 17 00:00:00 2001 From: Chang You Date: Mon, 10 Jul 2023 15:06:15 -0700 Subject: [PATCH 07/11] Updated Context, added spire_consumer_example.go --- .../spire_consumer_example.go | 158 ++++++++++++++++++ .../spire_producer.example.go | 5 +- 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 examples/spire_consumer_example/spire_consumer_example.go diff --git a/examples/spire_consumer_example/spire_consumer_example.go b/examples/spire_consumer_example/spire_consumer_example.go new file mode 100644 index 000000000..dd578292b --- /dev/null +++ b/examples/spire_consumer_example/spire_consumer_example.go @@ -0,0 +1,158 @@ +package main + +import ( + "context" + "fmt" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/spiffe/go-spiffe/v2/workloadapi" + "os" + "os/signal" + "syscall" + "time" +) + +// handleJWTTokenRefreshEvent retrieves JWT from the SPIRE workload API and +// sets the token on the client for use in any future authentication attempt. +// It must be invoked whenever kafka.OAuthBearerTokenRefresh appears on the client's event channel, +// which will occur whenever the client requires a token (i.e. when it first starts and when the +// previously-received token is 80% of the way to its expiration time). +func handleJWTTokenRefreshEvent(ctx context.Context, client kafka.Handle, principal, socketPath string, audience []string) { + fmt.Fprintf(os.Stderr, "Token refresh\n") + oauthBearerToken, closer, retrieveErr := retrieveJWTToken(ctx, principal, socketPath, audience) + defer closer() + if retrieveErr != nil { + fmt.Fprintf(os.Stderr, "%% Token retrieval error: %v\n", retrieveErr) + client.SetOAuthBearerTokenFailure(retrieveErr.Error()) + } else { + setTokenError := client.SetOAuthBearerToken(oauthBearerToken) + if setTokenError != nil { + fmt.Fprintf(os.Stderr, "%% Error setting token and extensions: %v\n", setTokenError) + client.SetOAuthBearerTokenFailure(setTokenError.Error()) + } + } +} + +func retrieveJWTToken(ctx context.Context, principal, socketPath string, audience []string) (kafka.OAuthBearerToken, func() error, error) { + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + jwtSource, err := workloadapi.NewJWTSource( + ctx, + workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), + ) + + if err != nil { + return kafka.OAuthBearerToken{}, nil, fmt.Errorf("unable to create JWTSource: %w", err) + } + + defer jwtSource.Close() + + params := jwtsvid.Params{ + Audience: audience[0], + // Other fields... + } + + jwtSVID, err := jwtSource.FetchJWTSVID(ctx, params) + if err != nil { + return kafka.OAuthBearerToken{}, nil, fmt.Errorf("unable to fetch JWT SVID: %w", err) + } + + extensions := map[string]string{ + "logicalCluster": "lkc-r6gdo0", + "identityPoolId": "pool-W9j5", + } + oauthBearerToken := kafka.OAuthBearerToken{ + TokenValue: jwtSVID.Marshal(), + Expiration: jwtSVID.Expiry, + Principal: principal, + Extensions: extensions, + } + + return oauthBearerToken, jwtSource.Close, nil +} + +func main() { + if len(os.Args) != 5 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + bootstrapServers := os.Args[1] + topic := os.Args[2] + principal := os.Args[3] + socketPath := os.Args[4] + audience := []string{"audience1", "audience2"} + + config := kafka.ConfigMap{ + "bootstrap.servers": bootstrapServers, + "security.protocol": "SASL_SSL", + "sasl.mechanisms": "OAUTHBEARER", + "sasl.oauthbearer.config": principal, + "group.id": "myGroup", + "session.timeout.ms": 6000, + "auto.offset.reset": "earliest", + "enable.auto.offset.store": false, + } + + c, err := kafka.NewConsumer(&config) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create consumer: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Created Consumer %v\n", c) + + ctx := context.Background() + go func() { + for { + ev := c.Poll(100) + switch e := ev.(type) { + case *kafka.Message: + fmt.Printf("%% Message on %s:\n%s\n", e.TopicPartition, string(e.Value)) + if e.Headers != nil { + fmt.Printf("%% Headers: %v\n", e.Headers) + } + _, err := c.StoreOffsets([]kafka.TopicPartition{e.TopicPartition}) + if err != nil { + fmt.Fprintf(os.Stderr, "%% Error storing offset: %v\n", err) + } + case kafka.Error: + // Errors should generally be considered + // informational, the client will try to + // automatically recover. + // But in this example we choose to terminate + // the application if all brokers are down. + fmt.Fprintf(os.Stderr, "%% Error: %v\n", e) + if e.Code() == kafka.ErrAllBrokersDown { + fmt.Fprintf(os.Stderr, "%% All brokers are down: terminating\n") + return + } + case kafka.OAuthBearerTokenRefresh: + handleJWTTokenRefreshEvent(ctx, c, principal, socketPath, audience) + default: + fmt.Printf("Ignored %v\n", e) + } + } + }() + + err = c.Subscribe(topic, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to subscribe to topic: %s\n", topic) + os.Exit(1) + } + + run := true + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) + + for run { + select { + case sig := <-signalChannel: + fmt.Printf("Caught signal %v: terminating\n", sig) + run = false + } + } + + fmt.Printf("Closing consumer\n") + c.Close() +} diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go index c1c6f2702..26ce77530 100644 --- a/examples/spire_producer.example/spire_producer.example.go +++ b/examples/spire_producer.example/spire_producer.example.go @@ -35,6 +35,8 @@ func handleJWTTokenRefreshEvent(ctx context.Context, client kafka.Handle, princi } func retrieveJWTToken(ctx context.Context, principal, socketPath string, audience []string) (kafka.OAuthBearerToken, func() error, error) { + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() jwtSource, err := workloadapi.NewJWTSource( ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), @@ -58,7 +60,7 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc } extensions := map[string]string{ - "logicalCluster": "lkc-vk3y7z", + "logicalCluster": "lkc-r6gdo0", "identityPoolId": "pool-W9j5", } oauthBearerToken := kafka.OAuthBearerToken{ @@ -102,6 +104,7 @@ func main() { // Token refresh events are posted on the Events channel, instructing // the application to refresh its token. ctx := context.Background() + go func(eventsChan chan kafka.Event) { for ev := range eventsChan { _, ok := ev.(kafka.OAuthBearerTokenRefresh) From 728a1557ebd958127befdcf69adc8963875faad0 Mon Sep 17 00:00:00 2001 From: Chang You Date: Tue, 11 Jul 2023 16:40:51 -0700 Subject: [PATCH 08/11] Updated spire_consumer_example.go and comment description. --- .../spire_consumer_example.go | 75 ++++++++++++------- .../spire_producer.example.go | 18 ++++- 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/examples/spire_consumer_example/spire_consumer_example.go b/examples/spire_consumer_example/spire_consumer_example.go index dd578292b..0b92e07de 100644 --- a/examples/spire_consumer_example/spire_consumer_example.go +++ b/examples/spire_consumer_example/spire_consumer_example.go @@ -1,3 +1,20 @@ +/** + * Copyright 2023 Confluent Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Example consumer with a custom SPIRE token implementation. package main import ( @@ -58,7 +75,7 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc } extensions := map[string]string{ - "logicalCluster": "lkc-r6gdo0", + "logicalCluster": "lkc-0yoqvq", "identityPoolId": "pool-W9j5", } oauthBearerToken := kafka.OAuthBearerToken{ @@ -95,6 +112,7 @@ func main() { } c, err := kafka.NewConsumer(&config) + if err != nil { fmt.Fprintf(os.Stderr, "Failed to create consumer: %s\n", err) os.Exit(1) @@ -102,19 +120,41 @@ func main() { fmt.Printf("Created Consumer %v\n", c) + err = c.SubscribeTopics([]string{topic}, nil) + + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to subscribe to topic: %s\n", topic) + os.Exit(1) + } + + run := true + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) + ctx := context.Background() - go func() { - for { + + for run { + select { + case sig := <-signalChannel: + fmt.Printf("Caught signal %v: terminating\n", sig) + run = false + default: ev := c.Poll(100) + if ev == nil { + continue + } + switch e := ev.(type) { case *kafka.Message: - fmt.Printf("%% Message on %s:\n%s\n", e.TopicPartition, string(e.Value)) + fmt.Printf("%% Message on %s:\n%s\n", + e.TopicPartition, string(e.Value)) if e.Headers != nil { fmt.Printf("%% Headers: %v\n", e.Headers) } - _, err := c.StoreOffsets([]kafka.TopicPartition{e.TopicPartition}) + _, err := c.StoreMessage(e) if err != nil { - fmt.Fprintf(os.Stderr, "%% Error storing offset: %v\n", err) + fmt.Fprintf(os.Stderr, "%% Error storing offset after message %s:\n", + e.TopicPartition) } case kafka.Error: // Errors should generally be considered @@ -122,10 +162,9 @@ func main() { // automatically recover. // But in this example we choose to terminate // the application if all brokers are down. - fmt.Fprintf(os.Stderr, "%% Error: %v\n", e) + fmt.Fprintf(os.Stderr, "%% Error: %v: %v\n", e.Code(), e) if e.Code() == kafka.ErrAllBrokersDown { - fmt.Fprintf(os.Stderr, "%% All brokers are down: terminating\n") - return + run = false } case kafka.OAuthBearerTokenRefresh: handleJWTTokenRefreshEvent(ctx, c, principal, socketPath, audience) @@ -133,24 +172,6 @@ func main() { fmt.Printf("Ignored %v\n", e) } } - }() - - err = c.Subscribe(topic, nil) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to subscribe to topic: %s\n", topic) - os.Exit(1) - } - - run := true - signalChannel := make(chan os.Signal, 1) - signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) - - for run { - select { - case sig := <-signalChannel: - fmt.Printf("Caught signal %v: terminating\n", sig) - run = false - } } fmt.Printf("Closing consumer\n") diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go index 26ce77530..33cf1e4fe 100644 --- a/examples/spire_producer.example/spire_producer.example.go +++ b/examples/spire_producer.example/spire_producer.example.go @@ -1,3 +1,19 @@ +/** + * Copyright 2023 Confluent Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Example producer with a custom SPIRE token implementation. package main import ( @@ -60,7 +76,7 @@ func retrieveJWTToken(ctx context.Context, principal, socketPath string, audienc } extensions := map[string]string{ - "logicalCluster": "lkc-r6gdo0", + "logicalCluster": "lkc-0yoqvq", "identityPoolId": "pool-W9j5", } oauthBearerToken := kafka.OAuthBearerToken{ From 92056a9abe1f492f5c5d51a88e5285cf9bb80228 Mon Sep 17 00:00:00 2001 From: Chang You Date: Wed, 13 Sep 2023 13:01:37 -0700 Subject: [PATCH 09/11] remove some unnecessary blocks --- .../spire_consumer_example.go | 24 ++++--------------- .../spire_producer.example.go | 2 +- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/examples/spire_consumer_example/spire_consumer_example.go b/examples/spire_consumer_example/spire_consumer_example.go index 0b92e07de..3e06ac6d5 100644 --- a/examples/spire_consumer_example/spire_consumer_example.go +++ b/examples/spire_consumer_example/spire_consumer_example.go @@ -29,7 +29,7 @@ import ( "time" ) -// handleJWTTokenRefreshEvent retrieves JWT from the SPIRE workload API and +// handleJWTTokenRefreshEvent retrieves JWT from the SPIFFE workload API and // sets the token on the client for use in any future authentication attempt. // It must be invoked whenever kafka.OAuthBearerTokenRefresh appears on the client's event channel, // which will occur whenever the client requires a token (i.e. when it first starts and when the @@ -101,14 +101,10 @@ func main() { audience := []string{"audience1", "audience2"} config := kafka.ConfigMap{ - "bootstrap.servers": bootstrapServers, - "security.protocol": "SASL_SSL", - "sasl.mechanisms": "OAUTHBEARER", - "sasl.oauthbearer.config": principal, - "group.id": "myGroup", - "session.timeout.ms": 6000, - "auto.offset.reset": "earliest", - "enable.auto.offset.store": false, + "bootstrap.servers": bootstrapServers, + "security.protocol": "SASL_SSL", + "sasl.mechanisms": "OAUTHBEARER", + "sasl.oauthbearer.config": principal, } c, err := kafka.NewConsumer(&config) @@ -151,21 +147,11 @@ func main() { if e.Headers != nil { fmt.Printf("%% Headers: %v\n", e.Headers) } - _, err := c.StoreMessage(e) - if err != nil { - fmt.Fprintf(os.Stderr, "%% Error storing offset after message %s:\n", - e.TopicPartition) - } case kafka.Error: // Errors should generally be considered // informational, the client will try to // automatically recover. - // But in this example we choose to terminate - // the application if all brokers are down. fmt.Fprintf(os.Stderr, "%% Error: %v: %v\n", e.Code(), e) - if e.Code() == kafka.ErrAllBrokersDown { - run = false - } case kafka.OAuthBearerTokenRefresh: handleJWTTokenRefreshEvent(ctx, c, principal, socketPath, audience) default: diff --git a/examples/spire_producer.example/spire_producer.example.go b/examples/spire_producer.example/spire_producer.example.go index 33cf1e4fe..6622c1063 100644 --- a/examples/spire_producer.example/spire_producer.example.go +++ b/examples/spire_producer.example/spire_producer.example.go @@ -29,7 +29,7 @@ import ( "github.com/confluentinc/confluent-kafka-go/v2/kafka" ) -// handleJWTTokenRefreshEvent retrieves JWT from the SPIRE workload API and +// handleJWTTokenRefreshEvent retrieves JWT from the SPIFFE workload API and // sets the token on the client for use in any future authentication attempt. // It must be invoked whenever kafka.OAuthBearerTokenRefresh appears on the client's event channel, // which will occur whenever the client requires a token (i.e. when it first starts and when the From decdb1257497212d76e3fdde137e69e76696ca70 Mon Sep 17 00:00:00 2001 From: chang-you Date: Mon, 9 Oct 2023 19:55:38 -0700 Subject: [PATCH 10/11] Empty-Commit From 89573cd70190307b6861ad6ee6ed9e18d401f036 Mon Sep 17 00:00:00 2001 From: chang-you Date: Mon, 9 Oct 2023 20:36:15 -0700 Subject: [PATCH 11/11] update CHANGELOG.md and examples/.gitignore --- CHANGELOG.md | 11 +++++++++++ examples/.gitignore | 2 ++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad6e4ce2..fff3448e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Confluent's Golang client for Apache Kafka +## v2.2.1 + +This is a maintenance release: + +* Bundles librdkafka v2.2.0. +* SPIRE example [producer](examples/spire_producer.example), [consumer](examples/spire_consumer_example) for clients to fetch JWT token by communicating with SPIRE agent. + + +confluent-kafka-go is based on librdkafka v2.2.0, see the +[librdkafka release notes](https://github.com/confluentinc/librdkafka/releases/tag/v2.2.0) +for a complete list of changes, enhancements, fixes and upgrade considerations. # v2.2.0 diff --git a/examples/.gitignore b/examples/.gitignore index 3519e7b27..acc862078 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -33,5 +33,7 @@ producer_custom_channel_example/producer_custom_channel_example producer_example/producer_example protobuf_consumer_example/protobuf_consumer_example protobuf_producer_example/protobuf_producer_example +spire_consumer_example/spire_consumer_example +spire_producer.example/spire_producer.example stats_example/stats_example transactions_example/transactions_example