206 lines
5.1 KiB
Go
206 lines
5.1 KiB
Go
package identity
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/golang-jwt/jwt/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestStripIncoming_RemovesSpoofedHeaders(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
r.Header.Set(HeaderUser, "evil-spoof")
|
||
|
|
r.Header.Set(HeaderRoles, "admin:*")
|
||
|
|
r.Header.Set(HeaderActingService, "fake-service")
|
||
|
|
r.Header.Set(HeaderAuthVerified, "true")
|
||
|
|
|
||
|
|
StripIncoming(r)
|
||
|
|
|
||
|
|
for _, h := range managed {
|
||
|
|
if got := r.Header.Get(h); got != "" {
|
||
|
|
t.Errorf("header %s should be stripped, got %q", h, got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStripIncoming_PreservesOtherHeaders(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
r.Header.Set("Authorization", "Bearer token")
|
||
|
|
r.Header.Set("Content-Type", "application/json")
|
||
|
|
r.Header.Set(HeaderUser, "spoof")
|
||
|
|
|
||
|
|
StripIncoming(r)
|
||
|
|
|
||
|
|
if got := r.Header.Get("Authorization"); got != "Bearer token" {
|
||
|
|
t.Errorf("Authorization should be preserved, got %q", got)
|
||
|
|
}
|
||
|
|
if got := r.Header.Get("Content-Type"); got != "application/json" {
|
||
|
|
t.Errorf("Content-Type should be preserved, got %q", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_ServiceAccount(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "abc123-hashed-id",
|
||
|
|
"azp": "portfolio-agent",
|
||
|
|
"roles": []interface{}{"llm:inference", "memory:read"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
assertHeader(t, r, HeaderAuthVerified, "true")
|
||
|
|
assertHeader(t, r, HeaderUser, "abc123-hashed-id")
|
||
|
|
assertHeader(t, r, HeaderRoles, "llm:inference,memory:read")
|
||
|
|
assertHeader(t, r, HeaderActingService, "portfolio-agent")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_HumanUser(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "user-hash-456",
|
||
|
|
"azp": "api-gw",
|
||
|
|
"permissions": []interface{}{"*"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
assertHeader(t, r, HeaderAuthVerified, "true")
|
||
|
|
assertHeader(t, r, HeaderUser, "user-hash-456")
|
||
|
|
assertHeader(t, r, HeaderRoles, "*")
|
||
|
|
// azp != sub, so acting-service is set
|
||
|
|
assertHeader(t, r, HeaderActingService, "api-gw")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_SameSubAndAzp_NoActingService(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "portfolio-agent",
|
||
|
|
"azp": "portfolio-agent",
|
||
|
|
"roles": []interface{}{"llm:inference"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
assertHeader(t, r, HeaderActingService, "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_RolesOverPermissions(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "user-1",
|
||
|
|
"roles": []interface{}{"llm:inference"},
|
||
|
|
"permissions": []interface{}{"admin:*"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
// roles takes precedence over permissions
|
||
|
|
assertHeader(t, r, HeaderRoles, "llm:inference")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_PermissionsFallback(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "user-1",
|
||
|
|
"permissions": []interface{}{"grafana:read", "grafana:write"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
assertHeader(t, r, HeaderRoles, "grafana:read,grafana:write")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_EmptyClaims(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
assertHeader(t, r, HeaderAuthVerified, "true")
|
||
|
|
assertHeader(t, r, HeaderUser, "")
|
||
|
|
assertHeader(t, r, HeaderRoles, "")
|
||
|
|
assertHeader(t, r, HeaderActingService, "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_NilValuesInClaims(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": nil,
|
||
|
|
"azp": nil,
|
||
|
|
"roles": nil,
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
assertHeader(t, r, HeaderAuthVerified, "true")
|
||
|
|
assertHeader(t, r, HeaderUser, "")
|
||
|
|
assertHeader(t, r, HeaderRoles, "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_WildcardPermission(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "admin-user",
|
||
|
|
"permissions": []interface{}{"*"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
// Wildcard passed as literal, never expanded
|
||
|
|
assertHeader(t, r, HeaderRoles, "*")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_MixedTypeRolesArray(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "user-1",
|
||
|
|
"roles": []interface{}{"llm:inference", 42, nil, "", "memory:read"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
// Non-string and empty elements skipped
|
||
|
|
assertHeader(t, r, HeaderRoles, "llm:inference,memory:read")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInject_EmptyRolesArray(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "user-1",
|
||
|
|
"roles": []interface{}{},
|
||
|
|
"permissions": []interface{}{"backup:read"},
|
||
|
|
}
|
||
|
|
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
// Empty roles falls through to permissions
|
||
|
|
assertHeader(t, r, HeaderRoles, "backup:read")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStripThenInject_OverwritesSpoof(t *testing.T) {
|
||
|
|
r := httptest.NewRequest("GET", "/", nil)
|
||
|
|
r.Header.Set(HeaderUser, "evil-spoof")
|
||
|
|
r.Header.Set(HeaderAuthVerified, "true")
|
||
|
|
|
||
|
|
StripIncoming(r)
|
||
|
|
|
||
|
|
claims := jwt.MapClaims{
|
||
|
|
"sub": "real-user",
|
||
|
|
"roles": []interface{}{"llm:inference"},
|
||
|
|
}
|
||
|
|
Inject(r, claims)
|
||
|
|
|
||
|
|
assertHeader(t, r, HeaderUser, "real-user")
|
||
|
|
assertHeader(t, r, HeaderAuthVerified, "true")
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertHeader(t *testing.T, r *http.Request, key, want string) {
|
||
|
|
t.Helper()
|
||
|
|
got := r.Header.Get(key)
|
||
|
|
if got != want {
|
||
|
|
t.Errorf("header %s = %q, want %q", key, got, want)
|
||
|
|
}
|
||
|
|
}
|