mirror of
https://github.com/alibaba/higress.git
synced 2026-06-04 01:57:26 +08:00
Signed-off-by: Betula-L <6059935+Betula-L@users.noreply.github.com> Co-authored-by: Betula-L <6059935+Betula-L@users.noreply.github.com>
289 lines
11 KiB
Go
289 lines
11 KiB
Go
// Copyright (c) 2023 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 config
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
const testJWKs = "{\"keys\":[{\"kty\":\"EC\",\"kid\":\"p256\",\"crv\":\"P-256\",\"x\":\"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\"y\":\"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"}]}"
|
|
|
|
func TestParseGlobalConfigRecordsRulesExist(t *testing.T) {
|
|
cfg := &JWTAuthConfig{}
|
|
err := ParseGlobalConfig(gjson.Parse(`{
|
|
"consumers": [{
|
|
"name": "inline-consumer",
|
|
"issuer": "higress-test",
|
|
"jwks": `+quoteJSON(testJWKs)+`
|
|
}],
|
|
"_rules_": [{
|
|
"_match_domain_": ["private.example.com"],
|
|
"allow": ["inline-consumer"]
|
|
}]
|
|
}`), cfg, nil)
|
|
|
|
if err != nil {
|
|
t.Fatalf("ParseGlobalConfig returned error: %v", err)
|
|
}
|
|
if !cfg.RuleSet {
|
|
t.Fatalf("expected global config to record that route/domain rules exist")
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerCachesInlineJWKs(t *testing.T) {
|
|
consumer, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "inline-consumer",
|
|
"issuer": "higress-test",
|
|
"jwks": `+quoteJSON(testJWKs)+`
|
|
}`), map[string]struct{}{})
|
|
|
|
if err != nil {
|
|
t.Fatalf("ParseConsumer returned error: %v", err)
|
|
}
|
|
if consumer.ParsedJWKs == nil || len(consumer.ParsedJWKs.Keys) != 1 {
|
|
t.Fatalf("expected parsed inline jwks to be cached, got: %#v", consumer.ParsedJWKs)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerTrimsIssuer(t *testing.T) {
|
|
consumer, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "inline-consumer",
|
|
"issuer": " higress-test ",
|
|
"jwks": `+quoteJSON(testJWKs)+`
|
|
}`), map[string]struct{}{})
|
|
|
|
if err != nil {
|
|
t.Fatalf("ParseConsumer returned error: %v", err)
|
|
}
|
|
if consumer.Issuer != "higress-test" {
|
|
t.Fatalf("expected issuer to be trimmed, got: %q", consumer.Issuer)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerAcceptsRemoteJWKsService(t *testing.T) {
|
|
consumer, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"remote_jwks": {
|
|
"service_name": "auth.example.com.dns",
|
|
"service_host": "auth.example.com",
|
|
"service_port": 443,
|
|
"path": "/.well-known/jwks.json"
|
|
}
|
|
}`), map[string]struct{}{})
|
|
|
|
if err != nil {
|
|
t.Fatalf("ParseConsumer returned error: %v", err)
|
|
}
|
|
if consumer.RemoteJWKs == nil {
|
|
t.Fatalf("expected remote_jwks to be parsed")
|
|
}
|
|
if consumer.RemoteJWKs.ServiceName != "auth.example.com.dns" {
|
|
t.Fatalf("unexpected service_name: %q", consumer.RemoteJWKs.ServiceName)
|
|
}
|
|
if consumer.RemoteJWKs.ServiceHost != "auth.example.com" {
|
|
t.Fatalf("unexpected service_host: %q", consumer.RemoteJWKs.ServiceHost)
|
|
}
|
|
if consumer.RemoteJWKs.ServicePort == nil || *consumer.RemoteJWKs.ServicePort != 443 {
|
|
t.Fatalf("unexpected service_port: %v", consumer.RemoteJWKs.ServicePort)
|
|
}
|
|
if consumer.RemoteJWKs.Path != "/.well-known/jwks.json" {
|
|
t.Fatalf("unexpected path: %q", consumer.RemoteJWKs.Path)
|
|
}
|
|
if got := *consumer.JWKsCacheDuration; got != 600 {
|
|
t.Fatalf("unexpected jwks_cache_duration: %d", got)
|
|
}
|
|
if got := *consumer.JWKsFetchTimeout; got != 1500 {
|
|
t.Fatalf("unexpected jwks_fetch_timeout: %d", got)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerTrimsRemoteJWKsServiceFields(t *testing.T) {
|
|
consumer, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"remote_jwks": {
|
|
"service_name": " auth.example.com.dns ",
|
|
"service_host": " auth.example.com ",
|
|
"path": " /.well-known/jwks.json "
|
|
}
|
|
}`), map[string]struct{}{})
|
|
|
|
if err != nil {
|
|
t.Fatalf("ParseConsumer returned error: %v", err)
|
|
}
|
|
if consumer.RemoteJWKs.ServiceName != "auth.example.com.dns" {
|
|
t.Fatalf("unexpected service_name: %q", consumer.RemoteJWKs.ServiceName)
|
|
}
|
|
if consumer.RemoteJWKs.ServiceHost != "auth.example.com" {
|
|
t.Fatalf("unexpected service_host: %q", consumer.RemoteJWKs.ServiceHost)
|
|
}
|
|
if consumer.RemoteJWKs.Path != "/.well-known/jwks.json" {
|
|
t.Fatalf("unexpected path: %q", consumer.RemoteJWKs.Path)
|
|
}
|
|
if consumer.RemoteJWKs.ServicePort == nil || *consumer.RemoteJWKs.ServicePort != 443 {
|
|
t.Fatalf("expected default service_port 443, got: %v", consumer.RemoteJWKs.ServicePort)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsBothInlineAndRemoteJWKs(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"jwks": `+quoteJSON(testJWKs)+`,
|
|
"remote_jwks": {"service_name": "auth.example.com.dns", "path": "/.well-known/jwks.json"}
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "only one of jwks and remote_jwks can be configured") {
|
|
t.Fatalf("expected mutually exclusive jwks error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsMissingJWKs(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test"
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "one of jwks and remote_jwks is required") {
|
|
t.Fatalf("expected missing jwks error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsRemoteJWKsWithoutIssuer(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"remote_jwks": {"service_name": "auth.example.com.dns", "path": "/.well-known/jwks.json"}
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "issuer is required when remote_jwks is set") {
|
|
t.Fatalf("expected missing issuer error for remote jwks, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsInvalidRemoteJWKsService(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
remoteJWKs string
|
|
}{
|
|
{name: "missing service_name", remoteJWKs: `"path": "/.well-known/jwks.json"`},
|
|
{name: "blank service_name", remoteJWKs: `"service_name": " ", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_name whitespace", remoteJWKs: `"service_name": "auth example", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_name cluster separator", remoteJWKs: `"service_name": "auth|example", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_name path", remoteJWKs: `"service_name": "auth.example.com/jwks", "path": "/.well-known/jwks.json"`},
|
|
{name: "missing service_host", remoteJWKs: `"service_name": "auth.example.com.dns", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_host whitespace", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth example", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_host scheme", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "https://auth.example.com", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_host port", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com:8443", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_host path", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com/jwks", "path": "/.well-known/jwks.json"`},
|
|
{name: "service_host userinfo", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "user@auth.example.com", "path": "/.well-known/jwks.json"`},
|
|
{name: "path control char", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/jwks\n.json"`},
|
|
{name: "path whitespace", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/jwks file.json"`},
|
|
{name: "missing path", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com"`},
|
|
{name: "relative path", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "jwks.json"`},
|
|
{name: "invalid port", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "service_port": 99999, "path": "/.well-known/jwks.json"`},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"remote_jwks": {`+tt.remoteJWKs+`}
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "remote_jwks is invalid") {
|
|
t.Fatalf("expected invalid remote_jwks error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsTooLargeRemoteJWKsFetchTimeout(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"remote_jwks": {"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/.well-known/jwks.json"},
|
|
"jwks_fetch_timeout": 10001
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "jwks_fetch_timeout must be less than or equal to") {
|
|
t.Fatalf("expected invalid jwks_fetch_timeout error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsTooLargeRemoteJWKsCacheDuration(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"remote_jwks": {"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/.well-known/jwks.json"},
|
|
"jwks_cache_duration": 604801
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "jwks_cache_duration must be less than or equal to") {
|
|
t.Fatalf("expected invalid jwks_cache_duration error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsTooSmallRemoteJWKsCacheDuration(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"remote_jwks": {"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/.well-known/jwks.json"},
|
|
"jwks_cache_duration": 29
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "jwks_cache_duration must be greater than or equal to 30") {
|
|
t.Fatalf("expected invalid jwks_cache_duration error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsRemoteJWKsOptionsForInlineJWKs(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "inline-consumer",
|
|
"issuer": "higress-test",
|
|
"jwks": `+quoteJSON(testJWKs)+`,
|
|
"jwks_cache_duration": 600
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "jwks_cache_duration and jwks_fetch_timeout only apply to remote_jwks") {
|
|
t.Fatalf("expected inline jwks remote option error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseConsumerRejectsEmptyInlineJWKs(t *testing.T) {
|
|
_, err := ParseConsumer(gjson.Parse(`{
|
|
"name": "remote-consumer",
|
|
"issuer": "higress-test",
|
|
"jwks": "{\"keys\":[]}"
|
|
}`), map[string]struct{}{})
|
|
|
|
if err == nil || !containsError(err, "jwks is empty") {
|
|
t.Fatalf("expected empty jwks error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func quoteJSON(value string) string {
|
|
return strconv.Quote(value)
|
|
}
|
|
|
|
func containsError(err error, want string) bool {
|
|
return err != nil && strings.Contains(err.Error(), want)
|
|
}
|