mirror of
https://github.com/alibaba/higress.git
synced 2026-06-26 02:35:02 +08:00
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
433 lines
14 KiB
Go
433 lines
14 KiB
Go
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package server
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"net/url"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// stubProvider implements SecuritySchemeProvider for ApplySecurity tests.
|
||
type stubProvider struct {
|
||
schemes map[string]SecurityScheme
|
||
}
|
||
|
||
func (p *stubProvider) GetSecurityScheme(id string) (SecurityScheme, bool) {
|
||
s, ok := p.schemes[id]
|
||
return s, ok
|
||
}
|
||
|
||
func newProvider(schemes ...SecurityScheme) *stubProvider {
|
||
m := make(map[string]SecurityScheme, len(schemes))
|
||
for _, s := range schemes {
|
||
m[s.ID] = s
|
||
}
|
||
return &stubProvider{schemes: m}
|
||
}
|
||
|
||
// mustParseURL helps build the ParsedURL field of AuthRequestContext.
|
||
func mustParseURL(t *testing.T, raw string) *url.URL {
|
||
t.Helper()
|
||
u, err := url.Parse(raw)
|
||
require.NoError(t, err)
|
||
return u
|
||
}
|
||
|
||
func findHeader(headers [][2]string, key string) (string, bool) {
|
||
for _, kv := range headers {
|
||
if strings.EqualFold(kv[0], key) {
|
||
return kv[1], true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
func countHeader(headers [][2]string, key string) int {
|
||
c := 0
|
||
for _, kv := range headers {
|
||
if strings.EqualFold(kv[0], key) {
|
||
c++
|
||
}
|
||
}
|
||
return c
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// setOrReplaceHeader
|
||
// -----------------------------------------------------------------------------
|
||
|
||
func TestSetOrReplaceHeader_AppendsWhenAbsent(t *testing.T) {
|
||
headers := [][2]string{{"X-Other", "1"}}
|
||
setOrReplaceHeader(&headers, "X-New", "v")
|
||
require.Len(t, headers, 2)
|
||
v, ok := findHeader(headers, "X-New")
|
||
require.True(t, ok)
|
||
assert.Equal(t, "v", v)
|
||
}
|
||
|
||
func TestSetOrReplaceHeader_ReplacesCaseInsensitively(t *testing.T) {
|
||
headers := [][2]string{
|
||
{"Content-Type", "text/plain"},
|
||
{"AUTHORIZATION", "old"},
|
||
}
|
||
setOrReplaceHeader(&headers, "authorization", "new")
|
||
v, ok := findHeader(headers, "Authorization")
|
||
require.True(t, ok)
|
||
assert.Equal(t, "new", v)
|
||
// Replacement is in-place, no duplicate header inserted.
|
||
assert.Equal(t, 1, countHeader(headers, "Authorization"))
|
||
assert.Len(t, headers, 2)
|
||
}
|
||
|
||
func TestSetOrReplaceHeader_PreservesOriginalKeyOnReplace(t *testing.T) {
|
||
headers := [][2]string{{"X-Token", "old"}}
|
||
setOrReplaceHeader(&headers, "x-token", "new")
|
||
// Replacement updates value but keeps the original key casing.
|
||
assert.Equal(t, [][2]string{{"X-Token", "new"}}, headers)
|
||
}
|
||
|
||
func TestSetOrReplaceHeader_FirstMatchWins(t *testing.T) {
|
||
headers := [][2]string{
|
||
{"X-Dup", "first"},
|
||
{"x-dup", "second"},
|
||
}
|
||
setOrReplaceHeader(&headers, "X-Dup", "new")
|
||
// Only the first occurrence is replaced; the second is left alone.
|
||
assert.Equal(t, [][2]string{{"X-Dup", "new"}, {"x-dup", "second"}}, headers)
|
||
}
|
||
|
||
func TestSetOrReplaceHeader_IdempotentOnSecondCall(t *testing.T) {
|
||
headers := [][2]string{}
|
||
setOrReplaceHeader(&headers, "X-K", "v")
|
||
setOrReplaceHeader(&headers, "X-K", "v")
|
||
require.Len(t, headers, 1)
|
||
assert.Equal(t, "v", headers[0][1])
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// ApplySecurity — early returns / preconditions
|
||
// -----------------------------------------------------------------------------
|
||
|
||
func TestApplySecurity_EmptyIDIsNoOp(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{
|
||
Headers: [][2]string{{"X-Other", "x"}},
|
||
ParsedURL: mustParseURL(t, "/p?a=1"),
|
||
}
|
||
err := ApplySecurity(SecurityRequirement{}, newProvider(), reqCtx)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, [][2]string{{"X-Other", "x"}}, reqCtx.Headers)
|
||
assert.Equal(t, "a=1", reqCtx.ParsedURL.RawQuery)
|
||
}
|
||
|
||
func TestApplySecurity_NilParsedURLReturnsError(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "x"},
|
||
newProvider(SecurityScheme{ID: "x", Type: "apiKey", In: "header", Name: "X"}),
|
||
reqCtx,
|
||
)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "ParsedURL")
|
||
}
|
||
|
||
func TestApplySecurity_SchemeIDNotFound(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(SecurityRequirement{ID: "missing"}, newProvider(), reqCtx)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "not found")
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// ApplySecurity — apiKey × {header, query}
|
||
// -----------------------------------------------------------------------------
|
||
|
||
func TestApplySecurity_ApiKey_Header_DefaultCredential(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{
|
||
Headers: [][2]string{{"X-Other", "x"}},
|
||
ParsedURL: mustParseURL(t, "/p"),
|
||
}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K"},
|
||
newProvider(SecurityScheme{
|
||
ID: "K", Type: "apiKey", In: "header", Name: "X-Api-Key",
|
||
DefaultCredential: "def",
|
||
}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, ok := findHeader(reqCtx.Headers, "X-Api-Key")
|
||
require.True(t, ok)
|
||
assert.Equal(t, "def", v)
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Header_ExplicitOverridesDefault(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "override"},
|
||
newProvider(SecurityScheme{
|
||
ID: "K", Type: "apiKey", In: "header", Name: "X-Api-Key",
|
||
DefaultCredential: "def",
|
||
}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "X-Api-Key")
|
||
assert.Equal(t, "override", v)
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Header_PassthroughBeatsExplicitAndDefault(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{
|
||
ParsedURL: mustParseURL(t, "/p"),
|
||
PassthroughCredential: "from-client",
|
||
}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "configured"},
|
||
newProvider(SecurityScheme{
|
||
ID: "K", Type: "apiKey", In: "header", Name: "X-Api-Key",
|
||
DefaultCredential: "def",
|
||
}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "X-Api-Key")
|
||
assert.Equal(t, "from-client", v, "passthrough wins over configured + default")
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Header_ReplacesExisting(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{
|
||
Headers: [][2]string{{"x-api-key", "stale"}},
|
||
ParsedURL: mustParseURL(t, "/p"),
|
||
}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "fresh"},
|
||
newProvider(SecurityScheme{
|
||
ID: "K", Type: "apiKey", In: "header", Name: "X-Api-Key",
|
||
}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
// Case-insensitive replace, no duplicate header.
|
||
assert.Equal(t, 1, countHeader(reqCtx.Headers, "X-Api-Key"))
|
||
v, _ := findHeader(reqCtx.Headers, "X-Api-Key")
|
||
assert.Equal(t, "fresh", v)
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_NoCredentialAvailable(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K"},
|
||
newProvider(SecurityScheme{ID: "K", Type: "apiKey", In: "header", Name: "X"}),
|
||
reqCtx,
|
||
)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "no credential")
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Header_MissingName(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "v"},
|
||
newProvider(SecurityScheme{ID: "K", Type: "apiKey", In: "header", Name: ""}),
|
||
reqCtx,
|
||
)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "name")
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Query_AppendsToExistingQuery(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p?existing=1")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "secret"},
|
||
newProvider(SecurityScheme{ID: "K", Type: "apiKey", In: "query", Name: "api_key"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
q := reqCtx.ParsedURL.Query()
|
||
assert.Equal(t, "secret", q.Get("api_key"))
|
||
assert.Equal(t, "1", q.Get("existing"), "existing query params must be preserved")
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Query_NoExistingQuery(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "secret"},
|
||
newProvider(SecurityScheme{ID: "K", Type: "apiKey", In: "query", Name: "api_key"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "api_key=secret", reqCtx.ParsedURL.RawQuery)
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Query_OverwritesExistingValue(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p?api_key=stale&keep=me")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "fresh"},
|
||
newProvider(SecurityScheme{ID: "K", Type: "apiKey", In: "query", Name: "api_key"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
q := reqCtx.ParsedURL.Query()
|
||
assert.Equal(t, "fresh", q.Get("api_key"))
|
||
assert.Equal(t, "me", q.Get("keep"))
|
||
// Sanity: no duplicate api_key entries.
|
||
assert.Len(t, q["api_key"], 1)
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_Query_MissingName(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "v"},
|
||
newProvider(SecurityScheme{ID: "K", Type: "apiKey", In: "query", Name: ""}),
|
||
reqCtx,
|
||
)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "name")
|
||
}
|
||
|
||
func TestApplySecurity_ApiKey_UnsupportedIn(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "K", Credential: "v"},
|
||
newProvider(SecurityScheme{ID: "K", Type: "apiKey", In: "cookie", Name: "X"}),
|
||
reqCtx,
|
||
)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "unsupported apiKey")
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// ApplySecurity — http × {bearer, basic}
|
||
// -----------------------------------------------------------------------------
|
||
|
||
func TestApplySecurity_HttpBearer_AddsPrefix(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B", Credential: "raw-token"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "bearer"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "Authorization")
|
||
assert.Equal(t, "Bearer raw-token", v)
|
||
}
|
||
|
||
func TestApplySecurity_HttpBearer_RespectsExistingPrefix(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B", Credential: "Bearer already-prefixed"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "bearer"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "Authorization")
|
||
assert.Equal(t, "Bearer already-prefixed", v, "must not double-prefix")
|
||
}
|
||
|
||
func TestApplySecurity_HttpBasic_UserPassEncoded(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B", Credential: "alice:s3cret"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "basic"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "Authorization")
|
||
expected := "Basic " + base64.StdEncoding.EncodeToString([]byte("alice:s3cret"))
|
||
assert.Equal(t, expected, v)
|
||
}
|
||
|
||
func TestApplySecurity_HttpBasic_PreEncodedToken(t *testing.T) {
|
||
// No colon → treated as already-base64 token.
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B", Credential: "QWxpY2U6czNjcmV0"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "basic"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "Authorization")
|
||
assert.Equal(t, "Basic QWxpY2U6czNjcmV0", v)
|
||
}
|
||
|
||
func TestApplySecurity_HttpBasic_RespectsExistingPrefix(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B", Credential: "Basic ZXhpc3Rpbmc="},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "basic"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "Authorization")
|
||
assert.Equal(t, "Basic ZXhpc3Rpbmc=", v, "must not re-encode already-prefixed value")
|
||
}
|
||
|
||
func TestApplySecurity_HttpBasic_PassthroughTreatedAsTokenPart(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{
|
||
ParsedURL: mustParseURL(t, "/p"),
|
||
PassthroughCredential: "QWxpY2U6czNjcmV0", // base64-encoded "alice:s3cret"
|
||
}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "basic"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "Authorization")
|
||
// Passthrough path must NOT re-base64-encode; only adds the prefix.
|
||
assert.Equal(t, "Basic QWxpY2U6czNjcmV0", v)
|
||
}
|
||
|
||
func TestApplySecurity_HttpBearer_Passthrough(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{
|
||
ParsedURL: mustParseURL(t, "/p"),
|
||
PassthroughCredential: "client-token",
|
||
}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "bearer"}),
|
||
reqCtx,
|
||
)
|
||
require.NoError(t, err)
|
||
v, _ := findHeader(reqCtx.Headers, "Authorization")
|
||
assert.Equal(t, "Bearer client-token", v)
|
||
}
|
||
|
||
func TestApplySecurity_HttpUnsupportedScheme(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B", Credential: "x"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "http", Scheme: "digest"}),
|
||
reqCtx,
|
||
)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "unsupported http scheme")
|
||
}
|
||
|
||
func TestApplySecurity_UnsupportedSchemeType(t *testing.T) {
|
||
reqCtx := &AuthRequestContext{ParsedURL: mustParseURL(t, "/p")}
|
||
err := ApplySecurity(
|
||
SecurityRequirement{ID: "B", Credential: "x"},
|
||
newProvider(SecurityScheme{ID: "B", Type: "oauth2"}),
|
||
reqCtx,
|
||
)
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "unsupported security scheme type")
|
||
}
|