From 014ebd519fcb6c7388e5862c7ab7be4275753520 Mon Sep 17 00:00:00 2001 From: Guilherme Carvalho Date: Mon, 17 Apr 2023 19:44:08 -0300 Subject: [PATCH 1/3] Add plugin templates authoring documentation (#47) Signed-off-by: Guilherme Carvalho --- docs/AUTHORING.md | 35 +++++++++-- docs/CONTRIBUTING.md | 2 +- docs/MIGRATING.md | 2 +- pluginmain/serve.go | 14 ++--- templates/agent/keymanager/keymanager.go | 49 ++++++++------- templates/agent/keymanager/keymanager_test.go | 23 ++++++- templates/agent/nodeattestor/nodeattestor.go | 40 ++++++------ .../agent/nodeattestor/nodeattestor_test.go | 20 +++++- templates/agent/svidstore/svidstore.go | 40 ++++++------ templates/agent/svidstore/svidstore_test.go | 19 +++++- .../workloadattestor/workloadattestor.go | 42 +++++++------ .../workloadattestor/workloadattestor_test.go | 18 +++++- .../credentialcomposer/credentialcomposer.go | 61 +++++++++++++------ .../credentialcomposer_test.go | 25 +++++++- templates/server/keymanager/keymanager.go | 49 ++++++++------- .../server/keymanager/keymanager_test.go | 23 ++++++- templates/server/nodeattestor/nodeattestor.go | 40 ++++++------ .../server/nodeattestor/nodeattestor_test.go | 20 +++++- templates/server/notifier/notifier.go | 41 +++++++------ templates/server/notifier/notifier_test.go | 23 ++++++- .../upstreamauthority/upstreamauthority.go | 54 ++++++++++------ .../upstreamauthority_test.go | 24 +++++++- 22 files changed, 465 insertions(+), 199 deletions(-) diff --git a/docs/AUTHORING.md b/docs/AUTHORING.md index 95b59b8..bc3d1fe 100644 --- a/docs/AUTHORING.md +++ b/docs/AUTHORING.md @@ -3,7 +3,34 @@ This document gives guidance for authoring plugins. SPIRE plugins implement one and only one plugin _type_ (e.g. KeyManager). They -also implement zero or more services. +also implement zero or more services. Below is a list of plugin types, alongside templates that can be used as a base +for authoring plugins. + +## Templates +Each template contains a go file that can be used as a starting point for authoring plugins. A test file is also +provided for each template; the test file contains a test suite that can be used to verify that the plugin has been +loaded and is working as expected using [plugintest](https://pkg.go.dev/github.com/spiffe/spire-plugin-sdk/plugintest). + +### Agent + +| Plugin | Description | Template | +|------------------|-------------------------------------------------------|---------------------------------------------| +| KeyManager | Manages private keys and performs signing operations. | [link](../templates/agent/keymanager) | +| NodeAttestor | Performs the agent side of the node attestation flow. | [link](../templates/agent/nodeattestor) | +| SVIDStore | Stores workload X509-SVIDs to arbitrary destinations. | [link](../templates/agent/svidstore) | +| WorkloadAttestor | Attests workloads and provides selectors. | [link](../templates/agent/workloadattestor) | + +### Server + +| Plugin | Description | Template | +|--------------------|--------------------------------------------------------|------------------------------------------------| +| KeyManager | Manages private keys and performs signing operations. | [link](../templates/server/keymanager) | +| NodeAttestor | Performs the server side of the node attestation flow. | [link](../templates/server/nodeattestor) | +| Notifier | Notifies external systems of certain SPIRE events. | [link](../templates/server/notifier) | +| UpstreamAuthority | Plugs SPIRE into an upstream PKI. | [link](../templates/server/upstreamauthority) | +| CredentialComposer | Allows customization of SVID and CA attributes. | [link](../templates/server/credentialcomposer) | + + ## Configuration @@ -69,7 +96,7 @@ func main() { plugin := new(Plugin) pluginmain.Serve( keymanagerv1.KeyManagerPluginServer(plugin), - configv1.ConfigPluginServer(plugin), // <-- add the Config service server implementation + configv1.ConfigServiceServer(plugin), // <-- add the Config service server implementation ) } ``` @@ -150,7 +177,7 @@ plugin will fail to load. ## Cleanup -Plugins are seperate processes and are terminated when the plugin is unloaded. +Plugins are separate processes and are terminated when the plugin is unloaded. However, it may be desirable to perform some graceful cleanup operations. To facilitate this, if plugin/service implementations implement the io.Closer @@ -176,7 +203,7 @@ See the package docs for more information. ## Running The [pluginmain](https://pkg.go.dev/github.com/spiffe/spire-plugin-sdk/pluginmain) package -is used to run the plugin. It takes care of setting up all of the plugin facilities and +is used to run the plugin. It takes care of setting up all the plugin facilities and wiring up the logger and hostservices. See the package docs for more information. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 42cd246..5f11ff7 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -52,5 +52,5 @@ SPIRE repository can be updated by running `go get github.com/spiffe/spire-plugin-sdk@next` from the SPIRE repository. Relying on a pseudo versions means that this repository only needs tags -for the offically released versions, while still allowing SPIRE to work with +for the officially released versions, while still allowing SPIRE to work with unreleased changes during development. diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index 9179e68..da2729b 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -125,7 +125,7 @@ to couple it to that operation. The `Attest` RPC request and response fields are now contained within `oneof`'s to strongly convey the difference in field requirements in requests and -responses during the atestation flow. The attestation payload no longer needs +responses during the attestation flow. The attestation payload no longer needs to include a type, since that is now inferred by SPIRE from the name of the plugin. The selectors returned in the final response are selector values only. The selector type is inferred by SPIRE from the name of the plugin. diff --git a/pluginmain/serve.go b/pluginmain/serve.go index ed2abe0..c32dbc9 100644 --- a/pluginmain/serve.go +++ b/pluginmain/serve.go @@ -8,13 +8,13 @@ import ( // Serve serves the plugin using the given plugin/service servers. It does // not return. It is intended to be called from main(). For example: // -// func main() { -// plugin := new(Plugin) -// pluginmain.Serve( -// keymanagerv1.KeyManagerPluginServer(plugin), -// configv1.ConfigPluginServer(plugin), -// ) -// } +// func main() { +// plugin := new(Plugin) +// pluginmain.Serve( +// keymanagerv1.KeyManagerPluginServer(plugin), +// configv1.ConfigServiceServer(plugin), +// ) +// } func Serve(pluginServer pluginsdk.PluginServer, serviceServers ...pluginsdk.ServiceServer) { logger := internal.NewLogger() internal.Serve(logger, logger, pluginServer, serviceServers, nil) diff --git a/templates/agent/keymanager/keymanager.go b/templates/agent/keymanager/keymanager.go index fbfc55d..a0f1fa8 100644 --- a/templates/agent/keymanager/keymanager.go +++ b/templates/agent/keymanager/keymanager.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,22 +50,8 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - -// GenerateKey implements the KeyManager GenerateKey RPC +// GenerateKey implements the KeyManager GenerateKey RPC. Generates a new private key with the given ID. +// If a key already exists under that ID, it is overwritten and given a different fingerprint. func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyRequest) (*keymanagerv1.GenerateKeyResponse, error) { config, err := p.getConfig() if err != nil { @@ -80,7 +66,8 @@ func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyR return nil, status.Error(codes.Unimplemented, "not implemented") } -// GetPublicKey implements the KeyManager GetPublicKey RPC +// GetPublicKey implements the KeyManager GetPublicKey RPC. Gets the public key information for the private key managed +// by the plugin with the given ID. If a key with the given ID does not exist, NOT_FOUND is returned. func (p *Plugin) GetPublicKey(ctx context.Context, req *keymanagerv1.GetPublicKeyRequest) (*keymanagerv1.GetPublicKeyResponse, error) { config, err := p.getConfig() if err != nil { @@ -95,7 +82,8 @@ func (p *Plugin) GetPublicKey(ctx context.Context, req *keymanagerv1.GetPublicKe return nil, status.Error(codes.Unimplemented, "not implemented") } -// GetPublicKeys implements the KeyManager GetPublicKeys RPC +// GetPublicKeys implements the KeyManager GetPublicKeys RPC. Gets all public key information for the private keys +// managed by the plugin. func (p *Plugin) GetPublicKeys(ctx context.Context, req *keymanagerv1.GetPublicKeysRequest) (*keymanagerv1.GetPublicKeysResponse, error) { config, err := p.getConfig() if err != nil { @@ -110,7 +98,9 @@ func (p *Plugin) GetPublicKeys(ctx context.Context, req *keymanagerv1.GetPublicK return nil, status.Error(codes.Unimplemented, "not implemented") } -// SignData implements the KeyManager SignData RPC +// SignData implements the KeyManager SignData RPC. Signs data with the private key identified by the given ID. If a key +// with the given ID does not exist, NOT_FOUND is returned. The response contains the signed data and the fingerprint of +// the key used to sign the data. See the PublicKey message for more details on the role of the fingerprint. func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest) (*keymanagerv1.SignDataResponse, error) { config, err := p.getConfig() if err != nil { @@ -126,7 +116,7 @@ func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -142,6 +132,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/agent/keymanager/keymanager_test.go b/templates/agent/keymanager/keymanager_test.go index fde3cfe..f572a51 100644 --- a/templates/agent/keymanager/keymanager_test.go +++ b/templates/agent/keymanager/keymanager_test.go @@ -1,6 +1,7 @@ package keymanager_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/agent/keymanager/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/agent/keymanager" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,23 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx := context.Background() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, kmClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + _, err = kmClient.GenerateKey(ctx, &keymanagerv1.GenerateKeyRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = kmClient.GetPublicKeys(ctx, &keymanagerv1.GetPublicKeysRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = kmClient.GetPublicKey(ctx, &keymanagerv1.GetPublicKeyRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = kmClient.SignData(ctx, &keymanagerv1.SignDataRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/agent/nodeattestor/nodeattestor.go b/templates/agent/nodeattestor/nodeattestor.go index 42a8028..9c205ee 100644 --- a/templates/agent/nodeattestor/nodeattestor.go +++ b/templates/agent/nodeattestor/nodeattestor.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,22 +50,9 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - -// AidAttestation implements the NodeAttestor AidAttestation RPC +// AidAttestation implements the NodeAttestor AidAttestation RPC. AidAttestation facilitates attestation by returning +// the attestation payload and participating in attestation challenge/response. This RPC uses a bidirectional stream for +// communication. func (p *Plugin) AidAttestation(stream nodeattestorv1.NodeAttestor_AidAttestationServer) error { config, err := p.getConfig() if err != nil { @@ -81,7 +68,7 @@ func (p *Plugin) AidAttestation(stream nodeattestorv1.NodeAttestor_AidAttestatio } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -97,6 +84,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/agent/nodeattestor/nodeattestor_test.go b/templates/agent/nodeattestor/nodeattestor_test.go index 71403ce..c8eb00b 100644 --- a/templates/agent/nodeattestor/nodeattestor_test.go +++ b/templates/agent/nodeattestor/nodeattestor_test.go @@ -1,6 +1,7 @@ package nodeattestor_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( nodeattestorv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/agent/nodeattestor/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/agent/nodeattestor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,20 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, naClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + stream, err := naClient.AidAttestation(ctx) + require.NoError(t, err) + _, err = stream.Recv() + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/agent/svidstore/svidstore.go b/templates/agent/svidstore/svidstore.go index 14689c7..7bff342 100644 --- a/templates/agent/svidstore/svidstore.go +++ b/templates/agent/svidstore/svidstore.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,22 +50,7 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - -// DeleteX509SVID implements the SVIDStore DeleteX509SVID RPC +// DeleteX509SVID implements the SVIDStore DeleteX509SVID RPC. Puts an X509-SVID in a configured secrets store. func (p *Plugin) DeleteX509SVID(ctx context.Context, req *svidstorev1.DeleteX509SVIDRequest) (*svidstorev1.DeleteX509SVIDResponse, error) { config, err := p.getConfig() if err != nil { @@ -80,7 +65,7 @@ func (p *Plugin) DeleteX509SVID(ctx context.Context, req *svidstorev1.DeleteX509 return nil, status.Error(codes.Unimplemented, "not implemented") } -// PutX509SVID implements the SVIDStore PutX509SVID RPC +// PutX509SVID implements the SVIDStore PutX509SVID RPC. Deletes an SVID from the store. func (p *Plugin) PutX509SVID(ctx context.Context, req *svidstorev1.PutX509SVIDRequest) (*svidstorev1.PutX509SVIDResponse, error) { config, err := p.getConfig() if err != nil { @@ -96,7 +81,7 @@ func (p *Plugin) PutX509SVID(ctx context.Context, req *svidstorev1.PutX509SVIDRe } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -112,6 +97,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/agent/svidstore/svidstore_test.go b/templates/agent/svidstore/svidstore_test.go index 775e3ac..37d07e3 100644 --- a/templates/agent/svidstore/svidstore_test.go +++ b/templates/agent/svidstore/svidstore_test.go @@ -1,6 +1,7 @@ package svidstore_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( svidstorev1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/agent/svidstore/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/agent/svidstore" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,19 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx := context.Background() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, ssClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + _, err = ssClient.PutX509SVID(ctx, &svidstorev1.PutX509SVIDRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = ssClient.DeleteX509SVID(ctx, &svidstorev1.DeleteX509SVIDRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/agent/workloadattestor/workloadattestor.go b/templates/agent/workloadattestor/workloadattestor.go index 430753d..8ed63be 100644 --- a/templates/agent/workloadattestor/workloadattestor.go +++ b/templates/agent/workloadattestor/workloadattestor.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,22 +50,11 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - -// Attest implements the WorkloadAttestor Attest RPC +// Attest implements the WorkloadAttestor Attest RPC. Attests the specified workload process. If the process is not one +// the attestor is in a position to attest (e.g. k8s attestor attesting a non-k8s workload), the call will succeed but +// return no selectors. If the process is one of the attestor is in a position to attest, but the attestor fails to +// gather all selectors related to that workload, the call will fail. Otherwise, the attestor will return one or more +// workload selectors. func (p *Plugin) Attest(ctx context.Context, req *workloadattestorv1.AttestRequest) (*workloadattestorv1.AttestResponse, error) { config, err := p.getConfig() if err != nil { @@ -81,7 +70,7 @@ func (p *Plugin) Attest(ctx context.Context, req *workloadattestorv1.AttestReque } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -97,6 +86,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/agent/workloadattestor/workloadattestor_test.go b/templates/agent/workloadattestor/workloadattestor_test.go index 9537b4b..c895c00 100644 --- a/templates/agent/workloadattestor/workloadattestor_test.go +++ b/templates/agent/workloadattestor/workloadattestor_test.go @@ -1,6 +1,7 @@ package workloadattestor_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( workloadattestorv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/agent/workloadattestor/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/agent/workloadattestor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,18 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, waClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + _, err = waClient.Attest(ctx, &workloadattestorv1.AttestRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/server/credentialcomposer/credentialcomposer.go b/templates/server/credentialcomposer/credentialcomposer.go index 7519577..0d2cc00 100644 --- a/templates/server/credentialcomposer/credentialcomposer.go +++ b/templates/server/credentialcomposer/credentialcomposer.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,21 +50,10 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - +// ComposeServerX509CA implements the CredentialComposer ComposeServerX509CA RPC. Composes the SPIRE Server X509 CA. +// The server will supply the default attributes it will apply to the CA. If the plugin returns an empty response or +// NOT_IMPLEMENTED, the server will apply the default attributes. Otherwise, the returned attributes are used. +// If a CA is produced that does not conform to the SPIFFE X509-SVID specification for signing certificates, it will be rejected. func (p *Plugin) ComposeServerX509CA(ctx context.Context, req *credentialcomposerv1.ComposeServerX509CARequest) (*credentialcomposerv1.ComposeServerX509CAResponse, error) { config, err := p.getConfig() if err != nil { @@ -79,6 +68,11 @@ func (p *Plugin) ComposeServerX509CA(ctx context.Context, req *credentialcompose return nil, status.Error(codes.Unimplemented, "not implemented") } +// ComposeServerX509SVID implements the CredentialComposer ComposeServerX509SVID RPC. Composes the SPIRE Server X509-SVID. +// The server will supply the default attributes it will apply to the server X509-SVID. If the plugin returns an empty +// response or NOT_IMPLEMENTED, the server will apply the default attributes. Otherwise, the returned attributes are +// used. If an X509-SVID is produced that does not conform to the SPIFFE X509-SVID specification for leaf certificates, +// it will be rejected. This function cannot be used to modify the SPIFFE ID of the X509-SVID. func (p *Plugin) ComposeServerX509SVID(ctx context.Context, req *credentialcomposerv1.ComposeServerX509SVIDRequest) (*credentialcomposerv1.ComposeServerX509SVIDResponse, error) { config, err := p.getConfig() if err != nil { @@ -93,6 +87,11 @@ func (p *Plugin) ComposeServerX509SVID(ctx context.Context, req *credentialcompo return nil, status.Error(codes.Unimplemented, "not implemented") } +// ComposeAgentX509SVID implements the CredentialComposer ComposeAgentX509SVID RPC. Composes the SPIRE Agent X509-SVID. +// The server will supply the default attributes it will apply to the agent X509-SVID. If the plugin returns an empty +// response or NOT_IMPLEMENTED, the server will apply the default attributes. Otherwise, the returned attributes are used. +// If an X509-SVID is produced that does not conform to the SPIFFE X509-SVID specification for leaf certificates, it will +// be rejected. This function cannot be used to modify the SPIFFE ID of the X509-SVID. func (p *Plugin) ComposeAgentX509SVID(ctx context.Context, req *credentialcomposerv1.ComposeAgentX509SVIDRequest) (*credentialcomposerv1.ComposeAgentX509SVIDResponse, error) { config, err := p.getConfig() if err != nil { @@ -107,6 +106,11 @@ func (p *Plugin) ComposeAgentX509SVID(ctx context.Context, req *credentialcompos return nil, status.Error(codes.Unimplemented, "not implemented") } +// ComposeWorkloadX509SVID implements the CredentialComposer ComposeWorkloadX509SVID RPC. Composes workload X509-SVIDs. +// The server will supply the default attributes it will apply to the workload X509-SVID. If the plugin returns an empty +// response or NOT_IMPLEMENTED, the server will apply the default attributes. Otherwise, the returned attributes are used. +// If an X509-SVID is produced that does not conform to the SPIFFE X509-SVID specification for leaf certificates, it will +// be rejected. This function cannot be used to modify the SPIFFE ID of the X509-SVID. func (p *Plugin) ComposeWorkloadX509SVID(ctx context.Context, req *credentialcomposerv1.ComposeWorkloadX509SVIDRequest) (*credentialcomposerv1.ComposeWorkloadX509SVIDResponse, error) { config, err := p.getConfig() if err != nil { @@ -120,6 +124,12 @@ func (p *Plugin) ComposeWorkloadX509SVID(ctx context.Context, req *credentialcom return nil, status.Error(codes.Unimplemented, "not implemented") } + +// ComposeWorkloadJWTSVID implements the CredentialComposer ComposeWorkloadJWTSVID RPC. Composes workload JWT-SVIDs. +// The server will supply the default attributes it will apply to the workload JWT-SVID. If the plugin returns an empty +// response or NOT_IMPLEMENTED, the server will apply the default attributes. Otherwise, the returned attributes are used. +// If a JWT-SVID is produced that does not conform to the SPIFFE JWT-SVID specification, it will be rejected. +// This function cannot be used to modify the SPIFFE ID of the JWT-SVID. func (p *Plugin) ComposeWorkloadJWTSVID(ctx context.Context, req *credentialcomposerv1.ComposeWorkloadJWTSVIDRequest) (*credentialcomposerv1.ComposeWorkloadJWTSVIDResponse, error) { config, err := p.getConfig() if err != nil { @@ -135,7 +145,7 @@ func (p *Plugin) ComposeWorkloadJWTSVID(ctx context.Context, req *credentialcomp } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -151,6 +161,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/server/credentialcomposer/credentialcomposer_test.go b/templates/server/credentialcomposer/credentialcomposer_test.go index a10551b..edad0e9 100644 --- a/templates/server/credentialcomposer/credentialcomposer_test.go +++ b/templates/server/credentialcomposer/credentialcomposer_test.go @@ -1,6 +1,7 @@ package credentialcomposer_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( credentialcomposerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/credentialcomposer/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/server/credentialcomposer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,25 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx := context.Background() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, pluginClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + _, err = pluginClient.ComposeServerX509CA(ctx, &credentialcomposerv1.ComposeServerX509CARequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = pluginClient.ComposeServerX509SVID(ctx, &credentialcomposerv1.ComposeServerX509SVIDRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = pluginClient.ComposeAgentX509SVID(ctx, &credentialcomposerv1.ComposeAgentX509SVIDRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = pluginClient.ComposeWorkloadX509SVID(ctx, &credentialcomposerv1.ComposeWorkloadX509SVIDRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = pluginClient.ComposeWorkloadJWTSVID(ctx, &credentialcomposerv1.ComposeWorkloadJWTSVIDRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/server/keymanager/keymanager.go b/templates/server/keymanager/keymanager.go index fd9fd28..6a03668 100644 --- a/templates/server/keymanager/keymanager.go +++ b/templates/server/keymanager/keymanager.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,22 +50,8 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - -// GenerateKey implements the KeyManager GenerateKey RPC +// GenerateKey implements the KeyManager GenerateKey RPC. Generates a new private key with the given ID. +// If a key already exists under that ID, it is overwritten and given a different fingerprint. func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyRequest) (*keymanagerv1.GenerateKeyResponse, error) { config, err := p.getConfig() if err != nil { @@ -80,7 +66,8 @@ func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyR return nil, status.Error(codes.Unimplemented, "not implemented") } -// GetPublicKey implements the KeyManager GetPublicKey RPC +// GetPublicKey implements the KeyManager GetPublicKey RPC. Gets the public key information for the private key managed +// by the plugin with the given ID. If a key with the given ID does not exist, NOT_FOUND is returned. func (p *Plugin) GetPublicKey(ctx context.Context, req *keymanagerv1.GetPublicKeyRequest) (*keymanagerv1.GetPublicKeyResponse, error) { config, err := p.getConfig() if err != nil { @@ -95,7 +82,8 @@ func (p *Plugin) GetPublicKey(ctx context.Context, req *keymanagerv1.GetPublicKe return nil, status.Error(codes.Unimplemented, "not implemented") } -// GetPublicKeys implements the KeyManager GetPublicKeys RPC +// GetPublicKeys implements the KeyManager GetPublicKeys RPC. Gets all public key information for the private keys +// managed by the plugin. func (p *Plugin) GetPublicKeys(ctx context.Context, req *keymanagerv1.GetPublicKeysRequest) (*keymanagerv1.GetPublicKeysResponse, error) { config, err := p.getConfig() if err != nil { @@ -110,7 +98,9 @@ func (p *Plugin) GetPublicKeys(ctx context.Context, req *keymanagerv1.GetPublicK return nil, status.Error(codes.Unimplemented, "not implemented") } -// SignData implements the KeyManager SignData RPC +// SignData implements the KeyManager SignData RPC. Signs data with the private key identified by the given ID. If a key +// with the given ID does not exist, NOT_FOUND is returned. The response contains the signed data and the fingerprint of +// the key used to sign the data. See the PublicKey message for more details on the role of the fingerprint. func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest) (*keymanagerv1.SignDataResponse, error) { config, err := p.getConfig() if err != nil { @@ -126,7 +116,7 @@ func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -142,6 +132,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/server/keymanager/keymanager_test.go b/templates/server/keymanager/keymanager_test.go index 717b083..d5d0b6c 100644 --- a/templates/server/keymanager/keymanager_test.go +++ b/templates/server/keymanager/keymanager_test.go @@ -1,6 +1,7 @@ package keymanager_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/server/keymanager" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,23 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx := context.Background() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, kmClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + _, err = kmClient.GenerateKey(ctx, &keymanagerv1.GenerateKeyRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = kmClient.GetPublicKeys(ctx, &keymanagerv1.GetPublicKeysRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = kmClient.GetPublicKey(ctx, &keymanagerv1.GetPublicKeyRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = kmClient.SignData(ctx, &keymanagerv1.SignDataRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/server/nodeattestor/nodeattestor.go b/templates/server/nodeattestor/nodeattestor.go index 4e721bb..4ad8314 100644 --- a/templates/server/nodeattestor/nodeattestor.go +++ b/templates/server/nodeattestor/nodeattestor.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,22 +50,9 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - -// Attest implements the NodeAttestor Attest RPC +// Attest implements the NodeAttestor Attest RPC. Attest attests attestation payload received from the agent and +// optionally participates in challenge/response attestation mechanics. This RPC uses a bidirectional stream for +// communication. func (p *Plugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) error { config, err := p.getConfig() if err != nil { @@ -81,7 +68,7 @@ func (p *Plugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) error { } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -97,6 +84,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/server/nodeattestor/nodeattestor_test.go b/templates/server/nodeattestor/nodeattestor_test.go index 9eabe16..a484a2d 100644 --- a/templates/server/nodeattestor/nodeattestor_test.go +++ b/templates/server/nodeattestor/nodeattestor_test.go @@ -1,6 +1,7 @@ package nodeattestor_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( nodeattestorv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/nodeattestor/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/server/nodeattestor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,20 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, naClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + stream, err := naClient.Attest(ctx) + require.NoError(t, err) + _, err = stream.Recv() + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/server/notifier/notifier.go b/templates/server/notifier/notifier.go index d93c0b5..aa7096b 100644 --- a/templates/server/notifier/notifier.go +++ b/templates/server/notifier/notifier.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,21 +50,8 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - +// Notify implements the Notifier Notify RPC. Notify notifies the plugin that an event occurred. Errors returned by +// the plugin are logged but otherwise ignored. func (p *Plugin) Notify(ctx context.Context, req *notifierv1.NotifyRequest) (*notifierv1.NotifyResponse, error) { config, err := p.getConfig() if err != nil { @@ -79,6 +66,9 @@ func (p *Plugin) Notify(ctx context.Context, req *notifierv1.NotifyRequest) (*no return nil, status.Error(codes.Unimplemented, "not implemented") } +// NotifyAndAdvise implements the Notifier NotifyAndAdvise RPC. NotifyAndAdvise notifies the plugin that an event +// occurred and waits for a response. Errors returned by the plugin control SPIRE Server behavior. +// See NotifyAndAdviseRequest for per-event details. func (p *Plugin) NotifyAndAdvise(ctx context.Context, req *notifierv1.NotifyAndAdviseRequest) (*notifierv1.NotifyAndAdviseResponse, error) { config, err := p.getConfig() if err != nil { @@ -94,7 +84,7 @@ func (p *Plugin) NotifyAndAdvise(ctx context.Context, req *notifierv1.NotifyAndA } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -110,6 +100,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/server/notifier/notifier_test.go b/templates/server/notifier/notifier_test.go index c3e1408..aae00c5 100644 --- a/templates/server/notifier/notifier_test.go +++ b/templates/server/notifier/notifier_test.go @@ -1,6 +1,7 @@ package notifier_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,11 +9,13 @@ import ( notifierv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/notifier/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/server/notifier" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { plugin := new(notifier.Plugin) - notClient := new(notifierv1.NotifierPluginClient) + ntClient := new(notifierv1.NotifierPluginClient) configClient := new(configv1.ConfigServiceClient) // Serve the plugin in the background with the configured plugin and @@ -23,7 +26,7 @@ func Test(t *testing.T) { // plugin. plugintest.ServeInBackground(t, plugintest.Config{ PluginServer: notifierv1.NotifierPluginServer(plugin), - PluginClient: notClient, + PluginClient: ntClient, ServiceServers: []pluginsdk.ServiceServer{ configv1.ConfigServiceServer(plugin), }, @@ -32,5 +35,19 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx := context.Background() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, ntClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + _, err = ntClient.Notify(ctx, ¬ifierv1.NotifyRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + _, err = ntClient.NotifyAndAdvise(ctx, ¬ifierv1.NotifyAndAdviseRequest{}) + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } diff --git a/templates/server/upstreamauthority/upstreamauthority.go b/templates/server/upstreamauthority/upstreamauthority.go index 7b527fd..b8f0e6d 100644 --- a/templates/server/upstreamauthority/upstreamauthority.go +++ b/templates/server/upstreamauthority/upstreamauthority.go @@ -15,12 +15,12 @@ import ( ) var ( - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsLogger interface. // TODO: Remove if the plugin does not need the logger. _ pluginsdk.NeedsLogger = (*Plugin)(nil) - // This compile time assertion ensures the plugin conforms properly to the + // This compile-time assertion ensures the plugin conforms properly to the // pluginsdk.NeedsHostServices interface. // TODO: Remove if the plugin does not need host services. _ pluginsdk.NeedsHostServices = (*Plugin)(nil) @@ -50,21 +50,14 @@ type Plugin struct { logger hclog.Logger } -// SetLogger is called by the framework when the plugin is loaded and provides -// the plugin with a logger wired up to SPIRE's logging facilities. -// TODO: Remove if the plugin does not need the logger. -func (p *Plugin) SetLogger(logger hclog.Logger) { - p.logger = logger -} - -// BrokerHostServices is called by the framework when the plugin is loaded to -// give the plugin a chance to obtain clients to SPIRE host services. -// TODO: Remove if the plugin does not need host services. -func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { - // TODO: Use the broker to obtain host service clients - return nil -} - +// MintX509CAAndSubscribe implements the UpstreamAuthority MintX509CAAndSubscribe RPC. Mints an X.509 CA and responds +// with the signed X.509 CA certificate chain and upstream X.509 roots. If supported by the implementation, subsequent +// responses on the stream contain upstream X.509 root updates, otherwise the stream is closed after the initial response. +// +// Implementation note: +// The stream should be kept open in the face of transient errors +// encountered while tracking changes to the upstream X.509 roots as SPIRE +// Server will not reopen a closed stream until the next X.509 CA rotation. func (p *Plugin) MintX509CAAndSubscribe(req *upstreamauthorityv1.MintX509CARequest, stream upstreamauthorityv1.UpstreamAuthority_MintX509CAAndSubscribeServer) error { config, err := p.getConfig() if err != nil { @@ -79,6 +72,16 @@ func (p *Plugin) MintX509CAAndSubscribe(req *upstreamauthorityv1.MintX509CAReque return status.Error(codes.Unimplemented, "not implemented") } +// PublishJWTKeyAndSubscribe implements the UpstreamAuthority PublishJWTKeyAndSubscribe RPC. Publishes a JWT signing key +// upstream and responds with the upstream JWT keys. If supported by the implementation, subsequent responses on the +// stream contain upstream JWT key updates, otherwise the stream is closed after the initial response. +// +// This RPC is optional and will return NotImplemented if unsupported. +// +// Implementation note: +// The stream should be kept open in the face of transient errors +// encountered while tracking changes to the upstream JWT keys as SPIRE +// Server will not reopen a closed stream until the next JWT key rotation. func (p *Plugin) PublishJWTKeyAndSubscribe(req *upstreamauthorityv1.PublishJWTKeyRequest, stream upstreamauthorityv1.UpstreamAuthority_PublishJWTKeyAndSubscribeServer) error { config, err := p.getConfig() if err != nil { @@ -94,7 +97,7 @@ func (p *Plugin) PublishJWTKeyAndSubscribe(req *upstreamauthorityv1.PublishJWTKe } // Configure configures the plugin. This is invoked by SPIRE when the plugin is -// first loaded. In the future, tt may be invoked to reconfigure the plugin. +// first loaded. In the future, it may be invoked to reconfigure the plugin. // As such, it should replace the previous configuration atomically. // TODO: Remove if no configuration is required func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { @@ -110,6 +113,21 @@ func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) return &configv1.ConfigureResponse{}, nil } +// BrokerHostServices is called by the framework when the plugin is loaded to +// give the plugin a chance to obtain clients to SPIRE host services. +// TODO: Remove if the plugin does not need host services. +func (p *Plugin) BrokerHostServices(broker pluginsdk.ServiceBroker) error { + // TODO: Use the broker to obtain host service clients + return nil +} + +// SetLogger is called by the framework when the plugin is loaded and provides +// the plugin with a logger wired up to SPIRE's logging facilities. +// TODO: Remove if the plugin does not need the logger. +func (p *Plugin) SetLogger(logger hclog.Logger) { + p.logger = logger +} + // setConfig replaces the configuration atomically under a write lock. // TODO: Remove if no configuration is required func (p *Plugin) setConfig(config *Config) { diff --git a/templates/server/upstreamauthority/upstreamauthority_test.go b/templates/server/upstreamauthority/upstreamauthority_test.go index 2102372..dbbf091 100644 --- a/templates/server/upstreamauthority/upstreamauthority_test.go +++ b/templates/server/upstreamauthority/upstreamauthority_test.go @@ -1,6 +1,7 @@ package upstreamauthority_test import ( + "context" "testing" "github.com/spiffe/spire-plugin-sdk/pluginsdk" @@ -8,6 +9,8 @@ import ( upstreamauthorityv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/upstreamauthority/v1" configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" "github.com/spiffe/spire-plugin-sdk/templates/server/upstreamauthority" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test(t *testing.T) { @@ -32,5 +35,24 @@ func Test(t *testing.T) { }, }) - // TODO: Invoke methods on the clients and assert the results + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // TODO: Remove if no configuration is required. + _, err := configClient.Configure(ctx, &configv1.ConfigureRequest{ + CoreConfiguration: &configv1.CoreConfiguration{TrustDomain: "example.org"}, + HclConfiguration: `{}`, + }) + assert.NoError(t, err) + + require.True(t, uaClient.IsInitialized()) + // TODO: Make assertions using the desired plugin behavior. + mintStream, err := uaClient.MintX509CAAndSubscribe(ctx, &upstreamauthorityv1.MintX509CARequest{}) + require.NoError(t, err) + _, err = mintStream.Recv() + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") + publishStream, err := uaClient.PublishJWTKeyAndSubscribe(ctx, &upstreamauthorityv1.PublishJWTKeyRequest{}) + require.NoError(t, err) + _, err = publishStream.Recv() + assert.EqualError(t, err, "rpc error: code = Unimplemented desc = not implemented") } From ef9c86293dfa29eb484421c1a96151b1c0e20cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Mart=C3=ADnez=20Fay=C3=B3?= Date: Thu, 22 Jun 2023 20:21:39 -0300 Subject: [PATCH 2/3] Introduce a helper package for BundlePublisher plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Agustín Martínez Fayó --- .../support/bundlepublisherutil/bputil.go | 307 ++++++++++++++++++ .../bundlepublisherutil/bputil_test.go | 139 ++++++++ 2 files changed, 446 insertions(+) create mode 100644 pluginsdk/support/bundlepublisherutil/bputil.go create mode 100644 pluginsdk/support/bundlepublisherutil/bputil_test.go diff --git a/pluginsdk/support/bundlepublisherutil/bputil.go b/pluginsdk/support/bundlepublisherutil/bputil.go new file mode 100644 index 0000000..95ab418 --- /dev/null +++ b/pluginsdk/support/bundlepublisherutil/bputil.go @@ -0,0 +1,307 @@ +// Package bundlepublisherutil provides helper functions for plugins +// implementing the BundlePublisher interface. +// BundlePublisher plugins should use this package as a way to have a +// standarized name for bundle formats in their configuration, and avoid the +// re-implementation of bundle parsing logic of formats supported in this +// package. +package bundlepublisherutil + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/types" + "gopkg.in/square/go-jose.v2" +) + +const ( + BundleFormatUnset BundleFormat = iota + SPIFFE + PEM + JWKS +) + +// Bundle represents a bundle that can be formatted in different formats. +type Bundle struct { + bundle *types.Bundle + + bytesMtx sync.RWMutex + jwksBytes []byte + pemBytes []byte + spiffeBytes []byte +} + +// KeyType represents the types of keys that are supported by the KeyManager. +type BundleFormat int + +// BundleFormatFromString returns the BundleFormat corresponding to the provided +// string. +func BundleFormatFromString(s string) (BundleFormat, error) { + switch strings.ToLower(s) { + case "spiffe": + return SPIFFE, nil + case "jwks": + return JWKS, nil + case "pem": + return PEM, nil + default: + return BundleFormatUnset, fmt.Errorf("unknown bundle format: %q", s) + } +} + +// NewBundle return a new *Bundle with the *types.Bundle provided. +// Use the Bytes() function to get a slice of bytes with the bundle formatted in +// the format specified. +func NewBundle(pluginBundle *types.Bundle) *Bundle { + return &Bundle{ + bundle: pluginBundle, + } +} + +// String returns the string name for the bundle format. +func (bundleFormat BundleFormat) String() string { + switch bundleFormat { + case BundleFormatUnset: + return "UNSET" + case SPIFFE: + return "spiffe" + case PEM: + return "pem" + case JWKS: + return "jwks" + default: + return fmt.Sprintf("UNKNOWN(%d)", int(bundleFormat)) + } +} + +// Bytes returns the bundle in the form of a slice of bytes in +// the chosen format. +func (b *Bundle) Bytes(format BundleFormat) ([]byte, error) { + if b.bundle == nil { + return nil, errors.New("missing bundle") + } + + switch format { + case BundleFormatUnset: + return nil, errors.New("no format specified") + + case JWKS: + if jwksBytes := b.getJWKSBytes(); jwksBytes != nil { + return jwksBytes, nil + } + jwksBytes, err := b.toJWKS() + if err != nil { + return nil, fmt.Errorf("could not convert bundle to jwks format: %w", err) + } + b.setJWKSBytes(jwksBytes) + return jwksBytes, nil + + case PEM: + if pemBytes := b.getPEMBytes(); pemBytes != nil { + return pemBytes, nil + } + + pemBytes, err := b.toPEM() + if err != nil { + return nil, fmt.Errorf("could not convert bundle to pem format: %w", err) + } + b.setPEMBytes(pemBytes) + return pemBytes, nil + + case SPIFFE: + if spiffeBytes := b.getSPIFFEBytes(); spiffeBytes != nil { + return spiffeBytes, nil + } + + spiffeBytes, err := b.toSPIFFEBundle() + if err != nil { + return nil, fmt.Errorf("could not convert bundle to spiffe format: %w", err) + } + b.setSPIFFEBytes(spiffeBytes) + return spiffeBytes, nil + + default: + return nil, fmt.Errorf("invalid format: %q", format) + } +} + +func (b *Bundle) getJWKSBytes() []byte { + b.bytesMtx.RLock() + defer b.bytesMtx.RUnlock() + + return b.jwksBytes +} + +func (b *Bundle) getPEMBytes() []byte { + b.bytesMtx.RLock() + defer b.bytesMtx.RUnlock() + + return b.pemBytes +} + +func (b *Bundle) getSPIFFEBytes() []byte { + b.bytesMtx.RLock() + defer b.bytesMtx.RUnlock() + + return b.spiffeBytes +} + +func (b *Bundle) setJWKSBytes(jwksBytes []byte) { + b.bytesMtx.Lock() + defer b.bytesMtx.Unlock() + + b.jwksBytes = jwksBytes +} + +func (b *Bundle) setPEMBytes(pemBytes []byte) { + b.bytesMtx.Lock() + defer b.bytesMtx.Unlock() + + b.pemBytes = pemBytes +} + +func (b *Bundle) setSPIFFEBytes(spiffeBytes []byte) { + b.bytesMtx.Lock() + defer b.bytesMtx.Unlock() + + b.spiffeBytes = spiffeBytes +} + +// toJWKS converts to JWKS the current bundle. +func (b *Bundle) toJWKS() ([]byte, error) { + var jwks jose.JSONWebKeySet + + x509Authorities, jwtAuthorities, err := getAuthorities(b.bundle) + if err != nil { + return nil, err + } + + for _, rootCA := range x509Authorities { + jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ + Key: rootCA.PublicKey, + Certificates: []*x509.Certificate{rootCA}, + }) + } + + for keyID, jwtSigningKey := range jwtAuthorities { + jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ + Key: jwtSigningKey, + KeyID: keyID, + }) + } + + var out interface{} = jwks + return json.MarshalIndent(out, "", " ") +} + +// toPEM converts to PEM the current bundle. +func (b *Bundle) toPEM() ([]byte, error) { + bundleData := new(bytes.Buffer) + for _, x509Authority := range b.bundle.X509Authorities { + if err := pem.Encode(bundleData, &pem.Block{ + Type: "CERTIFICATE", + Bytes: x509Authority.Asn1, + }); err != nil { + return nil, fmt.Errorf("could not perform PEM encoding: %w", err) + } + } + + return bundleData.Bytes(), nil +} + +// toSPIFFEBundle converts to a SPIFFE bundle the current bundle. +func (b *Bundle) toSPIFFEBundle() ([]byte, error) { + sb, err := spiffeBundleFromPluginProto(b.bundle) + if err != nil { + return nil, fmt.Errorf("failed to convert bundle: %w", err) + } + docBytes, err := sb.Marshal() + if err != nil { + return nil, fmt.Errorf("failed to marshal bundle: %w", err) + } + + var o bytes.Buffer + if err := json.Indent(&o, docBytes, "", " "); err != nil { + return nil, err + } + + return o.Bytes(), nil +} + +// getAuthorities gets the X.509 authorities and JWT authorities from the +// provided *types.Bundle. +func getAuthorities(bundleProto *types.Bundle) ([]*x509.Certificate, map[string]crypto.PublicKey, error) { + x509Authorities, err := x509CertificatesFromProto(bundleProto.X509Authorities) + if err != nil { + return nil, nil, err + } + jwtAuthorities, err := jwtKeysFromProto(bundleProto.JwtAuthorities) + if err != nil { + return nil, nil, err + } + + return x509Authorities, jwtAuthorities, nil +} + +// jwtKeysFromProto converts JWT keys from the given []*types.JWTKey to +// map[string]crypto.PublicKey. +// The key ID of the public key is used as the key in the returned map. +func jwtKeysFromProto(proto []*types.JWTKey) (map[string]crypto.PublicKey, error) { + keys := make(map[string]crypto.PublicKey) + for i, publicKey := range proto { + jwtSigningKey, err := x509.ParsePKIXPublicKey(publicKey.PublicKey) + if err != nil { + return nil, fmt.Errorf("unable to parse JWT signing key %d: %w", i, err) + } + keys[publicKey.KeyId] = jwtSigningKey + } + return keys, nil +} + +// spiffeBundleFromPluginProto converts a bundle from the given *types.Bundle to +// *spiffebundle.Bundle. +func spiffeBundleFromPluginProto(bundleProto *types.Bundle) (*spiffebundle.Bundle, error) { + td, err := spiffeid.TrustDomainFromString(bundleProto.TrustDomain) + if err != nil { + return nil, err + } + x509Authorities, jwtAuthorities, err := getAuthorities(bundleProto) + if err != nil { + return nil, err + } + + bundle := spiffebundle.New(td) + bundle.SetX509Authorities(x509Authorities) + bundle.SetJWTAuthorities(jwtAuthorities) + if bundleProto.RefreshHint > 0 { + bundle.SetRefreshHint(time.Duration(bundleProto.RefreshHint) * time.Second) + } + if bundleProto.SequenceNumber > 0 { + bundle.SetSequenceNumber(bundleProto.SequenceNumber) + } + return bundle, nil +} + +// x509CertificatesFromProto converts X.509 certificates from the given +// []*types.X509Certificate to []*x509.Certificate. +func x509CertificatesFromProto(proto []*types.X509Certificate) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for i, auth := range proto { + cert, err := x509.ParseCertificate(auth.Asn1) + if err != nil { + return nil, fmt.Errorf("unable to parse root CA %d: %w", i, err) + } + certs = append(certs, cert) + } + return certs, nil +} diff --git a/pluginsdk/support/bundlepublisherutil/bputil_test.go b/pluginsdk/support/bundlepublisherutil/bputil_test.go new file mode 100644 index 0000000..dfee5c1 --- /dev/null +++ b/pluginsdk/support/bundlepublisherutil/bputil_test.go @@ -0,0 +1,139 @@ +package bundlepublisherutil + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "math" + "testing" + + "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/types" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestBytes(t *testing.T) { + const ( + certPEM = `-----BEGIN CERTIFICATE----- +MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa +GA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyv +sCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXs +RxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkw +F4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09X +makw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylA +dZglS5kKnYigmwDh+/U= +-----END CERTIFICATE----- +` + ) + block, _ := pem.Decode([]byte(certPEM)) + require.NotNil(t, block, "unable to unmarshal certificate response: malformed PEM block") + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + + keyPkix, err := x509.MarshalPKIXPublicKey(cert.PublicKey) + require.NoError(t, err) + + testBundle := &types.Bundle{ + TrustDomain: "example.org", + X509Authorities: []*types.X509Certificate{{Asn1: cert.Raw}}, + JwtAuthorities: []*types.JWTKey{ + { + KeyId: "KID", + PublicKey: keyPkix, + }, + }, + RefreshHint: 1440, + SequenceNumber: 100, + } + standardJWKS := `{ + "keys": [ + { + %s"kty": "EC", + "crv": "P-256", + "x": "fK-wKTnKL7KFLM27lqq5DC-bxrVaH6rDV-IcCSEOeL4", + "y": "wq-g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KI", + "x5c": [ + "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyvsCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09Xmakw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylAdZglS5kKnYigmwDh+/U=" + ] + }, + { + %s"kty": "EC", + "kid": "KID", + "crv": "P-256", + "x": "fK-wKTnKL7KFLM27lqq5DC-bxrVaH6rDV-IcCSEOeL4", + "y": "wq-g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KI" + } + ]%s +}` + expectedJWKS := fmt.Sprintf(standardJWKS, "", "", "") + expectedSPIFFEBundle := fmt.Sprintf(standardJWKS, + `"use": "x509-svid", + `, + `"use": "jwt-svid", + `, + `, + "spiffe_sequence": 100, + "spiffe_refresh_hint": 1440`, + ) + + for _, tt := range []struct { + name string + format BundleFormat + bundle *types.Bundle + expectBytes []byte + expectError string + }{ + { + name: "format not set", + bundle: testBundle, + expectError: "no format specified", + }, + { + name: "invalid format", + format: math.MaxInt, + bundle: testBundle, + expectError: fmt.Sprintf("invalid format: \"UNKNOWN(%d)\"", math.MaxInt), + }, + { + name: "no bundle", + format: SPIFFE, + expectError: "missing bundle", + }, + { + name: "jwks format", + format: JWKS, + bundle: testBundle, + expectBytes: []byte(expectedJWKS), + }, + { + name: "pem format", + format: PEM, + bundle: testBundle, + expectBytes: []byte(certPEM), + }, + { + name: "spiffe format", + format: SPIFFE, + bundle: testBundle, + expectBytes: []byte(expectedSPIFFEBundle), + }, + } { + t.Run(tt.name, func(t *testing.T) { + b := NewBundle(tt.bundle) + + if !proto.Equal(tt.bundle, b.bundle) { + require.Equal(t, tt.bundle, b.bundle) + } + + bytes, err := b.Bytes(tt.format) + if tt.expectError != "" { + require.EqualError(t, err, tt.expectError) + require.Nil(t, bytes) + return + } + require.NoError(t, err) + require.Equal(t, string(tt.expectBytes), string(bytes)) + }) + } +} From 4cc3e30317829101f8cd9ee0972c541a8a2fef03 Mon Sep 17 00:00:00 2001 From: Edwin Buck Date: Fri, 28 Jun 2024 02:49:23 -0500 Subject: [PATCH 3/3] Adds Validate RPC to the Config service. --- .../service/common/config/v1/config.pb.go | 211 ++++++++++++++++-- .../service/common/config/v1/config.proto | 20 ++ .../common/config/v1/config_grpc.pb.go | 40 ++++ 3 files changed, 248 insertions(+), 23 deletions(-) diff --git a/proto/spire/service/common/config/v1/config.pb.go b/proto/spire/service/common/config/v1/config.pb.go index 1b9b585..8725d15 100644 --- a/proto/spire/service/common/config/v1/config.pb.go +++ b/proto/spire/service/common/config/v1/config.pb.go @@ -115,6 +115,120 @@ func (*ConfigureResponse) Descriptor() ([]byte, []int) { return file_spire_service_common_config_v1_config_proto_rawDescGZIP(), []int{1} } +type ValidateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Required. Core SPIRE configuration. + CoreConfiguration *CoreConfiguration `protobuf:"bytes,1,opt,name=core_configuration,json=coreConfiguration,proto3" json:"core_configuration,omitempty"` + // Required. HCL encoded plugin configuration. + HclConfiguration string `protobuf:"bytes,2,opt,name=hcl_configuration,json=hclConfiguration,proto3" json:"hcl_configuration,omitempty"` +} + +func (x *ValidateRequest) Reset() { + *x = ValidateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_spire_service_common_config_v1_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ValidateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateRequest) ProtoMessage() {} + +func (x *ValidateRequest) ProtoReflect() protoreflect.Message { + mi := &file_spire_service_common_config_v1_config_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateRequest.ProtoReflect.Descriptor instead. +func (*ValidateRequest) Descriptor() ([]byte, []int) { + return file_spire_service_common_config_v1_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ValidateRequest) GetCoreConfiguration() *CoreConfiguration { + if x != nil { + return x.CoreConfiguration + } + return nil +} + +func (x *ValidateRequest) GetHclConfiguration() string { + if x != nil { + return x.HclConfiguration + } + return "" +} + +type ValidateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Required. True when the plugin deems the configuration usable. + Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` + // Required. Text message suitable for presentation to end user. + TextMessage string `protobuf:"bytes,2,opt,name=text_message,json=textMessage,proto3" json:"text_message,omitempty"` +} + +func (x *ValidateResponse) Reset() { + *x = ValidateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_spire_service_common_config_v1_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ValidateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateResponse) ProtoMessage() {} + +func (x *ValidateResponse) ProtoReflect() protoreflect.Message { + mi := &file_spire_service_common_config_v1_config_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateResponse.ProtoReflect.Descriptor instead. +func (*ValidateResponse) Descriptor() ([]byte, []int) { + return file_spire_service_common_config_v1_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ValidateResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +func (x *ValidateResponse) GetTextMessage() string { + if x != nil { + return x.TextMessage + } + return "" +} + type CoreConfiguration struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -128,7 +242,7 @@ type CoreConfiguration struct { func (x *CoreConfiguration) Reset() { *x = CoreConfiguration{} if protoimpl.UnsafeEnabled { - mi := &file_spire_service_common_config_v1_config_proto_msgTypes[2] + mi := &file_spire_service_common_config_v1_config_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -141,7 +255,7 @@ func (x *CoreConfiguration) String() string { func (*CoreConfiguration) ProtoMessage() {} func (x *CoreConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_spire_service_common_config_v1_config_proto_msgTypes[2] + mi := &file_spire_service_common_config_v1_config_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -154,7 +268,7 @@ func (x *CoreConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use CoreConfiguration.ProtoReflect.Descriptor instead. func (*CoreConfiguration) Descriptor() ([]byte, []int) { - return file_spire_service_common_config_v1_config_proto_rawDescGZIP(), []int{2} + return file_spire_service_common_config_v1_config_proto_rawDescGZIP(), []int{4} } func (x *CoreConfiguration) GetTrustDomain() string { @@ -183,18 +297,40 @@ var file_spire_service_common_config_v1_config_proto_rawDesc = []byte{ 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x68, 0x63, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x13, 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x0a, 0x11, 0x43, 0x6f, 0x72, 0x65, 0x43, 0x6f, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa0, 0x01, 0x0a, 0x0f, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x60, 0x0a, 0x12, 0x63, 0x6f, + 0x72, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, 0x63, 0x6f, 0x72, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x11, + 0x68, 0x63, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x68, 0x63, 0x6c, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4b, 0x0a, 0x10, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x74, 0x65, 0x78, 0x74, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x36, 0x0a, 0x11, 0x43, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x72, 0x75, 0x73, 0x74, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x74, 0x72, 0x75, 0x73, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x32, 0x7a, - 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x70, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x75, 0x72, 0x65, 0x12, 0x30, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, + 0x09, 0x52, 0x0b, 0x74, 0x72, 0x75, 0x73, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x32, 0xe9, + 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x70, 0x0a, 0x09, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x12, 0x30, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x08, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, - 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x52, 0x5a, 0x50, 0x67, 0x69, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x73, 0x70, 0x69, 0x72, 0x65, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x52, 0x5a, 0x50, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x2f, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2d, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x70, 0x69, 0x72, 0x65, 0x2f, 0x73, 0x65, 0x72, @@ -215,21 +351,26 @@ func file_spire_service_common_config_v1_config_proto_rawDescGZIP() []byte { return file_spire_service_common_config_v1_config_proto_rawDescData } -var file_spire_service_common_config_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_spire_service_common_config_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_spire_service_common_config_v1_config_proto_goTypes = []interface{}{ (*ConfigureRequest)(nil), // 0: spire.service.common.config.v1.ConfigureRequest (*ConfigureResponse)(nil), // 1: spire.service.common.config.v1.ConfigureResponse - (*CoreConfiguration)(nil), // 2: spire.service.common.config.v1.CoreConfiguration + (*ValidateRequest)(nil), // 2: spire.service.common.config.v1.ValidateRequest + (*ValidateResponse)(nil), // 3: spire.service.common.config.v1.ValidateResponse + (*CoreConfiguration)(nil), // 4: spire.service.common.config.v1.CoreConfiguration } var file_spire_service_common_config_v1_config_proto_depIdxs = []int32{ - 2, // 0: spire.service.common.config.v1.ConfigureRequest.core_configuration:type_name -> spire.service.common.config.v1.CoreConfiguration - 0, // 1: spire.service.common.config.v1.Config.Configure:input_type -> spire.service.common.config.v1.ConfigureRequest - 1, // 2: spire.service.common.config.v1.Config.Configure:output_type -> spire.service.common.config.v1.ConfigureResponse - 2, // [2:3] is the sub-list for method output_type - 1, // [1:2] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 4, // 0: spire.service.common.config.v1.ConfigureRequest.core_configuration:type_name -> spire.service.common.config.v1.CoreConfiguration + 4, // 1: spire.service.common.config.v1.ValidateRequest.core_configuration:type_name -> spire.service.common.config.v1.CoreConfiguration + 0, // 2: spire.service.common.config.v1.Config.Configure:input_type -> spire.service.common.config.v1.ConfigureRequest + 2, // 3: spire.service.common.config.v1.Config.Validate:input_type -> spire.service.common.config.v1.ValidateRequest + 1, // 4: spire.service.common.config.v1.Config.Configure:output_type -> spire.service.common.config.v1.ConfigureResponse + 3, // 5: spire.service.common.config.v1.Config.Validate:output_type -> spire.service.common.config.v1.ValidateResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_spire_service_common_config_v1_config_proto_init() } @@ -263,6 +404,30 @@ func file_spire_service_common_config_v1_config_proto_init() { } } file_spire_service_common_config_v1_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ValidateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_spire_service_common_config_v1_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ValidateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_spire_service_common_config_v1_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CoreConfiguration); i { case 0: return &v.state @@ -281,7 +446,7 @@ func file_spire_service_common_config_v1_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_spire_service_common_config_v1_config_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/spire/service/common/config/v1/config.proto b/proto/spire/service/common/config/v1/config.proto index 878524f..dc66821 100644 --- a/proto/spire/service/common/config/v1/config.proto +++ b/proto/spire/service/common/config/v1/config.proto @@ -12,6 +12,10 @@ service Config { // calls to Configure can happen concurrently with other RPCs against the // plugin. rpc Configure(ConfigureRequest) returns (ConfigureResponse); + + // Validate is called by SPIRE with a potential specific configuration for + // the plugin to determine if it is usable. + rpc Validate(ValidateRequest) returns (ValidateResponse); } message ConfigureRequest { @@ -25,6 +29,22 @@ message ConfigureRequest { message ConfigureResponse { } +message ValidateRequest { + // Required. Core SPIRE configuration. + CoreConfiguration core_configuration = 1; + + // Required. HCL encoded plugin configuration. + string hcl_configuration = 2; +} + +message ValidateResponse { + // Required. True when the plugin deems the configuration usable. + bool valid = 1; + + // Required. Text message suitable for presentation to end user. + string text_message = 2; +} + message CoreConfiguration { // Required. The trust domain name SPIRE is configured with (e.g. // "example.org"). diff --git a/proto/spire/service/common/config/v1/config_grpc.pb.go b/proto/spire/service/common/config/v1/config_grpc.pb.go index c51079f..47a0101 100644 --- a/proto/spire/service/common/config/v1/config_grpc.pb.go +++ b/proto/spire/service/common/config/v1/config_grpc.pb.go @@ -26,6 +26,9 @@ type ConfigClient interface { // calls to Configure can happen concurrently with other RPCs against the // plugin. Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) + // Validate is called by SPIRE with a potential specific configuration for + // the plugin to determine if it is usable. + Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateResponse, error) } type configClient struct { @@ -45,6 +48,15 @@ func (c *configClient) Configure(ctx context.Context, in *ConfigureRequest, opts return out, nil } +func (c *configClient) Validate(ctx context.Context, in *ValidateRequest, opts ...grpc.CallOption) (*ValidateResponse, error) { + out := new(ValidateResponse) + err := c.cc.Invoke(ctx, "/spire.service.common.config.v1.Config/Validate", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ConfigServer is the server API for Config service. // All implementations must embed UnimplementedConfigServer // for forward compatibility @@ -57,6 +69,9 @@ type ConfigServer interface { // calls to Configure can happen concurrently with other RPCs against the // plugin. Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) + // Validate is called by SPIRE with a potential specific configuration for + // the plugin to determine if it is usable. + Validate(context.Context, *ValidateRequest) (*ValidateResponse, error) mustEmbedUnimplementedConfigServer() } @@ -67,6 +82,9 @@ type UnimplementedConfigServer struct { func (UnimplementedConfigServer) Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Configure not implemented") } +func (UnimplementedConfigServer) Validate(context.Context, *ValidateRequest) (*ValidateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Validate not implemented") +} func (UnimplementedConfigServer) mustEmbedUnimplementedConfigServer() {} // UnsafeConfigServer may be embedded to opt out of forward compatibility for this service. @@ -98,6 +116,24 @@ func _Config_Configure_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } +func _Config_Validate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ConfigServer).Validate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/spire.service.common.config.v1.Config/Validate", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ConfigServer).Validate(ctx, req.(*ValidateRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Config_ServiceDesc is the grpc.ServiceDesc for Config service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -109,6 +145,10 @@ var Config_ServiceDesc = grpc.ServiceDesc{ MethodName: "Configure", Handler: _Config_Configure_Handler, }, + { + MethodName: "Validate", + Handler: _Config_Validate_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "spire/service/common/config/v1/config.proto",