consul/agent/structs/config_entry_jwt_provider_t...

463 lines
12 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"testing"
"time"
"github.com/hashicorp/consul/acl"
"github.com/stretchr/testify/require"
)
func newTestAuthz(t *testing.T, src string) acl.Authorizer {
policy, err := acl.NewPolicyFromSource(src, nil, nil)
require.NoError(t, err)
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
require.NoError(t, err)
return authorizer
}
var tenSeconds time.Duration = 10 * time.Second
var hundredSeconds time.Duration = 100 * time.Second
var connectTimeout = time.Duration(5) * time.Second
func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
defaultMeta := DefaultEnterpriseMetaInDefaultPartition()
cases := map[string]configEntryTestcase{
"valid jwt-provider - local jwks": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "test-jwt-provider",
JSONWebKeySet: &JSONWebKeySet{
Local: &LocalJWKS{
Filename: "jwks.txt",
},
},
},
expected: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "test-jwt-provider",
JSONWebKeySet: &JSONWebKeySet{
Local: &LocalJWKS{
Filename: "jwks.txt",
},
},
ClockSkewSeconds: DefaultClockSkewSeconds,
EnterpriseMeta: *defaultMeta,
},
},
"valid jwt-provider - remote jwks defaults": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "test-jwt-provider",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
},
},
Locations: []*JWTLocation{
{
Header: &JWTLocationHeader{
Name: "Authorization",
},
},
},
Forwarding: &JWTForwardingConfig{
HeaderName: "Some-Header",
},
},
expected: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "test-jwt-provider",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
},
},
Forwarding: &JWTForwardingConfig{
HeaderName: "Some-Header",
},
Locations: []*JWTLocation{
{
Header: &JWTLocationHeader{
Name: "Authorization",
},
},
},
ClockSkewSeconds: DefaultClockSkewSeconds,
EnterpriseMeta: *defaultMeta,
},
},
"invalid jwt-provider - no name": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "",
},
validateErr: "Name is required",
},
"invalid jwt-provider - no jwks": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
},
validateErr: "JSONWebKeySet is required",
},
"invalid jwt-provider - no jwks local or remote set": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{},
},
validateErr: "must specify exactly one of Local or Remote JSON Web key set",
},
"invalid jwt-provider - local jwks with non-encoded base64 jwks": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Local: &LocalJWKS{
JWKS: "not base64 encoded",
},
},
},
validateErr: "JWKS must be valid base64 encoded string",
},
"invalid jwt-provider - both jwks local and remote set": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Local: &LocalJWKS{
Filename: "jwks.txt",
},
Remote: &RemoteJWKS{},
},
},
validateErr: "must specify exactly one of Local or Remote JSON Web key set",
},
"invalid jwt-provider - local jwks string and filename both set": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Local: &LocalJWKS{
Filename: "jwks.txt",
JWKS: "d2VhcmV0ZXN0aW5n",
},
},
},
validateErr: "must specify exactly one of String or filename for local keyset",
},
"invalid jwt-provider - remote jwks missing uri": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
},
},
},
validateErr: "Remote JWKS URI is required",
},
"invalid jwt-provider - remote jwks invalid uri": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "jibberishUrl",
},
},
},
validateErr: "Remote JWKS URI is invalid",
},
"invalid jwt-provider - JWT location with all fields": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
},
},
Locations: []*JWTLocation{
{
Header: &JWTLocationHeader{
Name: "Authorization",
},
QueryParam: &JWTLocationQueryParam{
Name: "TOKEN-QUERY",
},
Cookie: &JWTLocationCookie{
Name: "SomeCookie",
},
},
},
},
validateErr: "must set exactly one of: JWT location header, query param or cookie",
},
"invalid jwt-provider - Remote JWKS retry policy maxinterval < baseInterval": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
RetryPolicy: &JWKSRetryPolicy{
RetryPolicyBackOff: &RetryPolicyBackOff{
BaseInterval: hundredSeconds,
MaxInterval: tenSeconds,
},
},
},
},
},
validateErr: "retry policy backoff's MaxInterval should be greater or equal to BaseInterval",
},
"invalid jwt-provider - Remote JWKS cluster wrong discovery type": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
JWKSCluster: &JWKSCluster{
DiscoveryType: "FAKE",
},
},
},
},
validateErr: "unsupported jwks cluster discovery type: \"FAKE\"",
},
"invalid jwt-provider - Remote JWKS cluster with both trustedCa and provider instance": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
JWKSCluster: &JWKSCluster{
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{},
CaCertificateProviderInstance: &JWKSTLSCertProviderInstance{},
},
},
},
},
},
validateErr: "must specify exactly one of: CaCertificateProviderInstance or TrustedCA for JKWS' TLSCertificates",
},
"invalid jwt-provider - Remote JWKS cluster with multiple trustedCa options": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
JWKSCluster: &JWKSCluster{
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
InlineString: "*****",
},
},
},
},
},
},
validateErr: "must specify exactly one of: Filename, EnvironmentVariable, InlineString or InlineBytes for JWKS' TrustedCA",
},
"invalid jwt-provider - JWT location with 2 fields": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
},
},
Locations: []*JWTLocation{
{
Header: &JWTLocationHeader{
Name: "Authorization",
},
QueryParam: &JWTLocationQueryParam{
Name: "TOKEN-QUERY",
},
},
},
},
validateErr: "must set exactly one of: JWT location header, query param or cookie",
},
"valid jwt-provider - with all possible fields": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "test-jwt-provider",
Issuer: "iss",
Audiences: []string{"api", "web"},
CacheConfig: &JWTCacheConfig{
Size: 30,
},
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
RetryPolicy: &JWKSRetryPolicy{
RetryPolicyBackOff: &RetryPolicyBackOff{
BaseInterval: tenSeconds,
MaxInterval: hundredSeconds,
},
},
JWKSCluster: &JWKSCluster{
DiscoveryType: "STATIC",
ConnectTimeout: connectTimeout,
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
},
},
},
},
},
Forwarding: &JWTForwardingConfig{
HeaderName: "Some-Header",
},
Locations: []*JWTLocation{
{
Cookie: &JWTLocationCookie{
Name: "SomeCookie",
},
},
},
ClockSkewSeconds: 20,
},
expected: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "test-jwt-provider",
Issuer: "iss",
Audiences: []string{"api", "web"},
CacheConfig: &JWTCacheConfig{
Size: 30,
},
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
RetryPolicy: &JWKSRetryPolicy{
RetryPolicyBackOff: &RetryPolicyBackOff{
BaseInterval: tenSeconds,
MaxInterval: hundredSeconds,
},
},
JWKSCluster: &JWKSCluster{
DiscoveryType: "STATIC",
ConnectTimeout: connectTimeout,
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
},
},
},
},
},
Forwarding: &JWTForwardingConfig{
HeaderName: "Some-Header",
},
Locations: []*JWTLocation{
{
Cookie: &JWTLocationCookie{
Name: "SomeCookie",
},
},
},
ClockSkewSeconds: 20,
EnterpriseMeta: *defaultMeta,
},
},
}
testConfigEntryNormalizeAndValidate(t, cases)
}
func TestJWTProviderConfigEntry_ACLs(t *testing.T) {
cases := []configEntryACLTestCase{
{
name: "jwt-provider",
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "test-provider",
JSONWebKeySet: &JSONWebKeySet{
Local: &LocalJWKS{
Filename: "jwks.txt",
},
},
},
expectACLs: []configEntryTestACL{
{
name: "no-authz",
authorizer: newTestAuthz(t, ``),
canRead: false,
canWrite: false,
},
{
name: "jwt-provider: any service write",
authorizer: newTestAuthz(t, `service "" { policy = "write" }`),
canRead: true,
canWrite: false,
},
{
name: "jwt-provider: specific service write",
authorizer: newTestAuthz(t, `service "web" { policy = "write" }`),
canRead: true,
canWrite: false,
},
{
name: "jwt-provider: any service prefix write",
authorizer: newTestAuthz(t, `service_prefix "" { policy = "write" }`),
canRead: true,
canWrite: false,
},
{
name: "jwt-provider: mesh read",
authorizer: newTestAuthz(t, `mesh = "read"`),
canRead: true,
canWrite: false,
},
{
name: "jwt-provider: mesh write",
authorizer: newTestAuthz(t, `mesh = "write"`),
canRead: true,
canWrite: true,
},
{
name: "jwt-provider: operator read",
authorizer: newTestAuthz(t, `operator = "read"`),
canRead: true,
canWrite: false,
},
{
name: "jwt-provider: operator write",
authorizer: newTestAuthz(t, `operator = "write"`),
canRead: true,
canWrite: true,
},
},
},
}
testConfigEntries_ListRelatedServices_AndACLs(t, cases)
}