Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ func main() {
cacertPath = flag.String("ca-cert", "/etc/proxy/ca/ca.crt", "path to CA certificate PEM")
cakeyPath = flag.String("ca-key", "/etc/proxy/ca/ca.key", "path to CA private key PEM")
listenAddr = flag.String("listen", ":8080", "listen address")
verbose = flag.Bool("verbose", false, "enable goproxy verbose request/response logging")
)
flag.Parse()

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
opts := &slog.HandlerOptions{}
if *verbose {
opts.Level = slog.LevelDebug
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
if *verbose {
logger.Debug("verbose logging enabled")
}

cfg, err := proxy.LoadConfig(*configPath)
if err != nil {
Expand All @@ -65,6 +73,10 @@ func main() {
os.Exit(1)
}

if *verbose {
proxySrv.SetVerbose(true)
}

httpSrv := &http.Server{
Addr: *listenAddr,
Handler: proxySrv.Handler(),
Expand Down
23 changes: 12 additions & 11 deletions internal/controller/claw_channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ import (

// channelSecretRole defines a secret role with its placeholder token for proxy injection.
type channelSecretRole struct {
Role string
Placeholder string
Role string
Placeholder string
EnvVarSuffix string
AllowedPaths []string
}

// channelDefault holds the inferred proxy and config defaults for a known messaging channel.
type channelDefault struct {
Type clawv1alpha1.CredentialType
Domain string
APIKey *clawv1alpha1.APIKeyConfig
PathToken *clawv1alpha1.PathTokenConfig
Companions []string // additional domains to allowlist (type: none)
SecretRoles []channelSecretRole
AllowedPaths []string // for the primary route only (e.g., Slack app-token path)
Type clawv1alpha1.CredentialType
Domain string
APIKey *clawv1alpha1.APIKeyConfig
PathToken *clawv1alpha1.PathTokenConfig
Companions []string // additional domains to allowlist (type: none)
SecretRoles []channelSecretRole

// ConfigBase is the base channel config block injected into operator.json.
// Keys like "enabled" and token placeholders are added by buildChannelConfig.
Expand Down Expand Up @@ -89,8 +90,8 @@ var knownChannels = map[string]channelDefault{
".slack.com",
},
SecretRoles: []channelSecretRole{
{Role: "botToken", Placeholder: "xoxb-placeholder"},
{Role: "appToken", Placeholder: "xapp-placeholder"},
{Role: "appToken", Placeholder: "xapp-placeholder", EnvVarSuffix: "APP", AllowedPaths: []string{"/api/apps.connections.open"}},
{Role: "botToken", Placeholder: "xoxb-placeholder", EnvVarSuffix: "BOT"},
},
ConfigBase: map[string]any{
"botToken": "xoxb-placeholder",
Expand Down
28 changes: 25 additions & 3 deletions internal/controller/claw_channels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ func TestChannelSecretRefHelpers(t *testing.T) {
assert.False(t, referencesSecret(cred, "any-secret"))
})

t.Run("proxySecretForCredential picks botToken for slack channel", func(t *testing.T) {
t.Run("proxySecretForCredential picks first role (appToken) for slack channel", func(t *testing.T) {
cred := clawv1alpha1.CredentialSpec{
Name: "slack",
Channel: "slack",
Expand All @@ -601,8 +601,8 @@ func TestChannelSecretRefHelpers(t *testing.T) {
}
ref := proxySecretForCredential(cred)
require.NotNil(t, ref)
assert.Equal(t, "bot-token", ref.Key)
assert.Equal(t, "botToken", ref.Role)
assert.Equal(t, "app-token", ref.Key)
assert.Equal(t, "appToken", ref.Role)
})

t.Run("proxySecretForCredential falls back to first entry for single-secret channel", func(t *testing.T) {
Expand Down Expand Up @@ -631,6 +631,28 @@ func TestChannelSecretRefHelpers(t *testing.T) {
})
}

func TestSlackSecretRolesRegistry(t *testing.T) {
slack, ok := knownChannels["slack"]
require.True(t, ok, "slack should be registered in knownChannels")
require.Len(t, slack.SecretRoles, 2, "slack should have two SecretRoles")

t.Run("appToken role has AllowedPaths and EnvVarSuffix", func(t *testing.T) {
role := slack.SecretRoles[0]
assert.Equal(t, "appToken", role.Role)
assert.Equal(t, "APP", role.EnvVarSuffix)
assert.Equal(t, []string{"/api/apps.connections.open"}, role.AllowedPaths)
assert.Equal(t, "xapp-placeholder", role.Placeholder)
})

t.Run("botToken role is the catch-all", func(t *testing.T) {
role := slack.SecretRoles[1]
assert.Equal(t, "botToken", role.Role)
assert.Equal(t, "BOT", role.EnvVarSuffix)
assert.Empty(t, role.AllowedPaths)
assert.Equal(t, "xoxb-placeholder", role.Placeholder)
})
}

// --- Integration tests: channel credentials through envtest reconciler ---

func TestChannelCredentialReconciliation(t *testing.T) {
Expand Down
96 changes: 74 additions & 22 deletions internal/controller/claw_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,25 @@ func generateProxyConfig(
return json.Marshal(cfg)
}

// multiSecretRoles returns the channel's SecretRoles when the credential belongs
// to a multi-secret channel (multiple roles with EnvVarSuffix). Returns nil for
// single-role channels and non-channel credentials.
func multiSecretRoles(cred clawv1alpha1.CredentialSpec) []channelSecretRole {
if cred.Channel == "" {
return nil
}
defaults, ok := knownChannels[cred.Channel]
if !ok || len(defaults.SecretRoles) < 2 {
return nil
}
for _, role := range defaults.SecretRoles {
if role.EnvVarSuffix != "" {
return defaults.SecretRoles
}
}
return nil
}

// credentialRoutes builds proxy routes from resolved credentials, returning
// exact-match and suffix-match routes separately for deterministic ordering.
func credentialRoutes(credentials []resolvedCredential) (exact, suffix []proxyRoute) {
Expand Down Expand Up @@ -202,18 +221,31 @@ func credentialRoutes(credentials []resolvedCredential) (exact, suffix []proxyRo
}

if cred.Domain != "" {
route := buildCredentialRoute(cred)
if roles := multiSecretRoles(cred); len(roles) > 0 {
for _, role := range roles {
route := buildCredentialRoute(cred)
route.EnvVar = credEnvVarName(cred.Name) + "_" + role.EnvVarSuffix
route.AllowedPaths = role.AllowedPaths
if strings.HasPrefix(cred.Domain, ".") {
suffix = append(suffix, route)
} else {
exact = append(exact, route)
}
}
} else {
route := buildCredentialRoute(cred)

if cred.Provider != "" && cred.Type != clawv1alpha1.CredentialTypePathToken && !usesVertexSDK(cred) {
info := resolveProviderInfo(cred)
route.PathPrefix = "/" + strings.ToLower(cred.Name)
route.Upstream = info.Upstream
}
if cred.Provider != "" && cred.Type != clawv1alpha1.CredentialTypePathToken && !usesVertexSDK(cred) {
info := resolveProviderInfo(cred)
route.PathPrefix = "/" + strings.ToLower(cred.Name)
route.Upstream = info.Upstream
}

if strings.HasPrefix(cred.Domain, ".") {
suffix = append(suffix, route)
} else {
exact = append(exact, route)
if strings.HasPrefix(cred.Domain, ".") {
suffix = append(suffix, route)
} else {
exact = append(exact, route)
}
}
}

Expand Down Expand Up @@ -492,24 +524,43 @@ func configureProxyForCredentials(objects []*unstructured.Unstructured, instance

for _, rc := range credentials {
cred := rc.CredentialSpec
ref := proxySecretForCredential(cred)
switch cred.Type {
case clawv1alpha1.CredentialTypeAPIKey, clawv1alpha1.CredentialTypeBearer,
clawv1alpha1.CredentialTypePathToken, clawv1alpha1.CredentialTypeOAuth2:
if ref == nil {
continue
}
envVars = append(envVars, map[string]any{
"name": credEnvVarName(cred.Name),
"valueFrom": map[string]any{
"secretKeyRef": map[string]any{
"name": ref.Name,
"key": ref.Key,
if roles := multiSecretRoles(cred); len(roles) > 0 {
for _, role := range roles {
ref := secretForRole(cred, role.Role)
if ref == nil {
continue
}
envVars = append(envVars, map[string]any{
"name": credEnvVarName(cred.Name) + "_" + role.EnvVarSuffix,
"valueFrom": map[string]any{
"secretKeyRef": map[string]any{
"name": ref.Name,
"key": ref.Key,
},
},
})
}
} else {
ref := proxySecretForCredential(cred)
if ref == nil {
continue
}
envVars = append(envVars, map[string]any{
"name": credEnvVarName(cred.Name),
"valueFrom": map[string]any{
"secretKeyRef": map[string]any{
"name": ref.Name,
"key": ref.Key,
},
},
},
})
})
}

case clawv1alpha1.CredentialTypeGCP:
ref := proxySecretForCredential(cred)
if ref == nil {
continue
}
Expand All @@ -533,6 +584,7 @@ func configureProxyForCredentials(objects []*unstructured.Unstructured, instance
})

case clawv1alpha1.CredentialTypeKubernetes:
ref := proxySecretForCredential(cred)
if ref == nil {
continue
}
Expand Down
Loading
Loading