guildhouse-spire-plugins/cmd/gsap-attestor/main.go
Tyler J King 68414987d5 fix(gsap-attestor): handle SPIRE's HCL v1 quoted-key format
SPIRE converts JSON plugin_data to HCL v1 native syntax with quoted
attribute names ("max_depth" = 10). HCL v2's parser rejects quoted
keys, so strip them before parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 08:43:05 -04:00

89 lines
2.6 KiB
Go

package main
import (
"context"
"regexp"
"sync"
"github.com/hashicorp/hcl/v2/hclsimple"
configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1"
workloadattestorv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/agent/workloadattestor/v1"
"github.com/spiffe/spire-plugin-sdk/pluginmain"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type Plugin struct {
workloadattestorv1.UnimplementedWorkloadAttestorServer
configv1.UnimplementedConfigServer
mu sync.RWMutex
attestor *GsapAttestor
}
func (p *Plugin) Attest(ctx context.Context, req *workloadattestorv1.AttestRequest) (*workloadattestorv1.AttestResponse, error) {
p.mu.RLock()
a := p.attestor
p.mu.RUnlock()
if a == nil {
return nil, status.Error(codes.FailedPrecondition, "plugin not configured")
}
selectors, err := a.Attest(ctx, req.Pid)
if err != nil {
return nil, status.Errorf(codes.Internal, "attestation failed: %v", err)
}
return &workloadattestorv1.AttestResponse{
SelectorValues: selectors,
}, nil
}
// unquoteHCLKeys strips quotes from HCL v1-style attribute names
// (e.g. `"max_depth" = 10` → `max_depth = 10`). SPIRE converts JSON
// plugin_data to HCL v1 native syntax which uses quoted keys that
// HCL v2's parser rejects.
var hclQuotedKey = regexp.MustCompile(`(?m)^(\s*)"([a-zA-Z_][a-zA-Z0-9_]*)"(\s*=)`)
func decodeConfig(data string) (GsapAttestorConfig, error) {
var cfg GsapAttestorConfig
normalized := hclQuotedKey.ReplaceAllString(data, `${1}${2}${3}`)
if err := hclsimple.Decode("plugin.hcl", []byte(normalized), nil, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
func (p *Plugin) Configure(_ context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) {
cfg, err := decodeConfig(req.HclConfiguration)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to decode configuration: %v", err)
}
a := &GsapAttestor{}
if err := a.Configure(cfg); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid configuration: %v", err)
}
p.mu.Lock()
p.attestor = a
p.mu.Unlock()
return &configv1.ConfigureResponse{}, nil
}
func (p *Plugin) Validate(_ context.Context, req *configv1.ValidateRequest) (*configv1.ValidateResponse, error) {
if _, err := decodeConfig(req.HclConfiguration); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to decode configuration: %v", err)
}
return &configv1.ValidateResponse{}, nil
}
func main() {
p := &Plugin{}
pluginmain.Serve(
workloadattestorv1.WorkloadAttestorPluginServer(p),
configv1.ConfigServiceServer(p),
)
}