mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
468 lines
13 KiB
Go
468 lines
13 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
)
|
|
|
|
// TestIssue55TemplateExecutionWithWrongTypes tests what happens when templates
|
|
// receive wrong data types during execution - this reproduces the exact error
|
|
// from GitHub issue #55: "can't evaluate field AccessToken in type bool"
|
|
func TestIssue55TemplateExecutionWithWrongTypes(t *testing.T) {
|
|
testCases := []struct {
|
|
templateData interface{}
|
|
name string
|
|
templateText string
|
|
errorContains string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "correct map data",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: map[string]interface{}{
|
|
"AccessToken": "valid-token",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "boolean as root context - reproduces issue #55",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: true,
|
|
expectError: true,
|
|
errorContains: "can't evaluate field AccessToken in type bool",
|
|
},
|
|
{
|
|
name: "string as root context",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: "just a string",
|
|
expectError: true,
|
|
errorContains: "can't evaluate field AccessToken in type string",
|
|
},
|
|
{
|
|
name: "nil as root context",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: nil,
|
|
expectError: false, // nil renders as <no value>
|
|
errorContains: "",
|
|
},
|
|
{
|
|
name: "map with wrong field type",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: map[string]interface{}{
|
|
"AccessToken": true, // boolean instead of string
|
|
},
|
|
expectError: false, // This should work, template will convert bool to string
|
|
},
|
|
{
|
|
name: "nested claims access with correct data",
|
|
templateText: "User: {{.Claims.email}}",
|
|
templateData: map[string]interface{}{
|
|
"Claims": map[string]interface{}{
|
|
"email": "[email protected]",
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "nested claims with wrong structure",
|
|
templateText: "User: {{.Claims.email}}",
|
|
templateData: map[string]interface{}{
|
|
"Claims": "not a map", // string instead of map
|
|
},
|
|
expectError: true,
|
|
errorContains: "can't evaluate field email in type", // interface{} or string
|
|
},
|
|
{
|
|
name: "array as root context",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: []string{"item1", "item2"},
|
|
expectError: true,
|
|
errorContains: "can't evaluate field AccessToken in type []string",
|
|
},
|
|
{
|
|
name: "integer as root context",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: 42,
|
|
expectError: true,
|
|
errorContains: "can't evaluate field AccessToken in type int",
|
|
},
|
|
{
|
|
name: "empty template data map",
|
|
templateText: "Bearer {{.AccessToken}}",
|
|
templateData: map[string]interface{}{},
|
|
expectError: false, // Should render as "Bearer <no value>"
|
|
},
|
|
{
|
|
name: "complex nested structure",
|
|
templateText: "{{.Claims.sub}} - {{.Claims.groups}} - {{.AccessToken}}",
|
|
templateData: map[string]interface{}{
|
|
"AccessToken": "token123",
|
|
"Claims": map[string]interface{}{
|
|
"sub": "user-id",
|
|
"groups": "admin,users",
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tmpl, err := template.New("test").Parse(tc.templateText)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = tmpl.Execute(&buf, tc.templateData)
|
|
|
|
if tc.expectError {
|
|
if err == nil {
|
|
t.Fatalf("Expected error but got none, output: %q", buf.String())
|
|
}
|
|
if tc.errorContains != "" && !strings.Contains(err.Error(), tc.errorContains) {
|
|
t.Errorf("Expected error to contain %q, got %q", tc.errorContains, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIssue55TemplateParsingValidation ensures templates are parsed correctly
|
|
// and validates the template data structure used in the middleware
|
|
func TestIssue55TemplateParsingValidation(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
headerTemplates []TemplatedHeader
|
|
shouldError bool
|
|
}{
|
|
{
|
|
name: "valid bearer token template",
|
|
headerTemplates: []TemplatedHeader{
|
|
{Name: "Authorization", Value: "Bearer {{.AccessToken}}"},
|
|
},
|
|
shouldError: false,
|
|
},
|
|
{
|
|
name: "multiple valid templates",
|
|
headerTemplates: []TemplatedHeader{
|
|
{Name: "Authorization", Value: "Bearer {{.AccessToken}}"},
|
|
{Name: "X-User-Email", Value: "{{.Claims.email}}"},
|
|
{Name: "X-User-ID", Value: "{{.Claims.sub}}"},
|
|
},
|
|
shouldError: false,
|
|
},
|
|
{
|
|
name: "template with conditional logic",
|
|
headerTemplates: []TemplatedHeader{
|
|
{Name: "X-Auth-Info", Value: "{{if .AccessToken}}Bearer {{.AccessToken}}{{else}}No Token{{end}}"},
|
|
},
|
|
shouldError: false,
|
|
},
|
|
{
|
|
name: "invalid template syntax",
|
|
headerTemplates: []TemplatedHeader{
|
|
{Name: "Bad-Template", Value: "{{.AccessToken"},
|
|
},
|
|
shouldError: true,
|
|
},
|
|
{
|
|
name: "empty template value",
|
|
headerTemplates: []TemplatedHeader{
|
|
{Name: "Empty-Header", Value: ""},
|
|
},
|
|
shouldError: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for _, header := range tc.headerTemplates {
|
|
tmpl, err := template.New(header.Name).Parse(header.Value)
|
|
|
|
if tc.shouldError {
|
|
if err == nil {
|
|
t.Errorf("Expected template parsing to fail for %s", header.Name)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Failed to parse template for header %s: %v", header.Name, err)
|
|
continue
|
|
}
|
|
|
|
// Test execution with correct data structure
|
|
templateData := map[string]interface{}{
|
|
"AccessToken": "test-access-token",
|
|
"IDToken": "test-id-token",
|
|
"RefreshToken": "test-refresh-token",
|
|
"Claims": map[string]interface{}{
|
|
"email": "[email protected]",
|
|
"sub": "user123",
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = tmpl.Execute(&buf, templateData)
|
|
if err != nil {
|
|
t.Errorf("Failed to execute valid template: %v", err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIssue55MiddlewareHeaderTemplating simulates the actual middleware flow
|
|
// to ensure templated headers work correctly in request processing
|
|
func TestIssue55MiddlewareHeaderTemplating(t *testing.T) {
|
|
// Test cases that simulate real-world usage
|
|
testCases := []struct {
|
|
claims map[string]interface{}
|
|
expectedValues map[string]string
|
|
name string
|
|
accessToken string
|
|
idToken string
|
|
headers []TemplatedHeader
|
|
}{
|
|
{
|
|
name: "authorization header with access token",
|
|
headers: []TemplatedHeader{
|
|
{Name: "Authorization", Value: "Bearer {{.AccessToken}}"},
|
|
},
|
|
accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
|
expectedValues: map[string]string{
|
|
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
|
},
|
|
},
|
|
{
|
|
name: "multiple headers with claims",
|
|
headers: []TemplatedHeader{
|
|
{Name: "X-User-Email", Value: "{{.Claims.email}}"},
|
|
{Name: "X-User-Groups", Value: "{{.Claims.groups}}"},
|
|
{Name: "X-Auth-Token", Value: "{{.AccessToken}}"},
|
|
},
|
|
accessToken: "token123",
|
|
claims: map[string]interface{}{
|
|
"email": "[email protected]",
|
|
"groups": "admin,developers",
|
|
},
|
|
expectedValues: map[string]string{
|
|
"X-User-Email": "[email protected]",
|
|
"X-User-Groups": "admin,developers",
|
|
"X-Auth-Token": "token123",
|
|
},
|
|
},
|
|
{
|
|
name: "complex template expressions",
|
|
headers: []TemplatedHeader{
|
|
{Name: "X-User-Info", Value: "{{.Claims.sub}} ({{.Claims.email}})"},
|
|
{Name: "X-Auth-Header", Value: "Bearer {{.AccessToken}} | ID: {{.IDToken}}"},
|
|
},
|
|
accessToken: "access-token",
|
|
idToken: "id-token",
|
|
claims: map[string]interface{}{
|
|
"sub": "user-12345",
|
|
"email": "[email protected]",
|
|
},
|
|
expectedValues: map[string]string{
|
|
"X-User-Info": "user-12345 ([email protected])",
|
|
"X-Auth-Header": "Bearer access-token | ID: id-token",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Parse all templates
|
|
headerTemplates := make(map[string]*template.Template)
|
|
for _, header := range tc.headers {
|
|
tmpl, err := template.New(header.Name).Parse(header.Value)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template for %s: %v", header.Name, err)
|
|
}
|
|
headerTemplates[header.Name] = tmpl
|
|
}
|
|
|
|
// Create template data (simulating what the middleware does)
|
|
templateData := map[string]interface{}{
|
|
"AccessToken": tc.accessToken,
|
|
"IDToken": tc.idToken,
|
|
"RefreshToken": "refresh-token", // Default value
|
|
"Claims": tc.claims,
|
|
}
|
|
|
|
// Create a test request
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
|
|
// Execute templates and set headers
|
|
for headerName, tmpl := range headerTemplates {
|
|
var buf bytes.Buffer
|
|
err := tmpl.Execute(&buf, templateData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute template for %s: %v", headerName, err)
|
|
}
|
|
req.Header.Set(headerName, buf.String())
|
|
}
|
|
|
|
// Verify all expected headers are set correctly
|
|
for headerName, expectedValue := range tc.expectedValues {
|
|
actualValue := req.Header.Get(headerName)
|
|
if actualValue != expectedValue {
|
|
t.Errorf("Header %s: expected %q, got %q", headerName, expectedValue, actualValue)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIssue55JSONConfigParsing tests that JSON configuration with wrong types
|
|
// is properly rejected to prevent the boolean type error
|
|
func TestIssue55JSONConfigParsing(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
jsonConfig string
|
|
description string
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "valid JSON configuration",
|
|
jsonConfig: `{
|
|
"headers": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "Bearer {{.AccessToken}}"
|
|
}
|
|
]
|
|
}`,
|
|
expectedError: false,
|
|
description: "Properly formatted JSON with string values",
|
|
},
|
|
{
|
|
name: "JSON with boolean value",
|
|
jsonConfig: `{
|
|
"headers": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": true
|
|
}
|
|
]
|
|
}`,
|
|
expectedError: true,
|
|
description: "Boolean value instead of string template",
|
|
},
|
|
{
|
|
name: "JSON with number value",
|
|
jsonConfig: `{
|
|
"headers": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": 123
|
|
}
|
|
]
|
|
}`,
|
|
expectedError: true,
|
|
description: "Number value instead of string template",
|
|
},
|
|
{
|
|
name: "JSON with null value",
|
|
jsonConfig: `{
|
|
"headers": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": null
|
|
}
|
|
]
|
|
}`,
|
|
expectedError: false, // JSON unmarshaling null to string results in empty string
|
|
description: "Null value instead of string template",
|
|
},
|
|
{
|
|
name: "JSON with array value",
|
|
jsonConfig: `{
|
|
"headers": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": ["Bearer", "{{.AccessToken}}"]
|
|
}
|
|
]
|
|
}`,
|
|
expectedError: true,
|
|
description: "Array value instead of string template",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var config struct {
|
|
Headers []TemplatedHeader `json:"headers"`
|
|
}
|
|
|
|
err := json.Unmarshal([]byte(tc.jsonConfig), &config)
|
|
|
|
if tc.expectedError {
|
|
if err == nil {
|
|
t.Errorf("Expected error for %s, but parsing succeeded", tc.description)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error for %s: %v", tc.description, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIssue55RegressionScenario tests the exact scenario that would cause
|
|
// the "can't evaluate field AccessToken in type bool" error
|
|
func TestIssue55RegressionScenario(t *testing.T) {
|
|
// This test documents what NOT to do and ensures we catch it
|
|
t.Run("direct boolean context execution", func(t *testing.T) {
|
|
tmpl, err := template.New("test").Parse("{{.AccessToken}}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
// This is what would cause the issue - passing a boolean as template data
|
|
err = tmpl.Execute(&buf, true)
|
|
|
|
if err == nil {
|
|
t.Fatalf("Expected error when executing template with boolean context")
|
|
}
|
|
|
|
expectedError := "can't evaluate field AccessToken in type bool"
|
|
if !strings.Contains(err.Error(), expectedError) {
|
|
t.Errorf("Expected error containing %q, got %q", expectedError, err.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("correct map context execution", func(t *testing.T) {
|
|
tmpl, err := template.New("test").Parse("{{.AccessToken}}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
// This is the correct way - passing a map with the expected fields
|
|
err = tmpl.Execute(&buf, map[string]interface{}{
|
|
"AccessToken": "test-token",
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error with correct template data: %v", err)
|
|
}
|
|
|
|
if buf.String() != "test-token" {
|
|
t.Errorf("Expected 'test-token', got %q", buf.String())
|
|
}
|
|
})
|
|
}
|