// 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 shortInterval = 10 * time.Second
var longInterval = 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: longInterval,
								MaxInterval:  shortInterval,
							},
						},
					},
				},
			},
			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: shortInterval,
								MaxInterval:  longInterval,
							},
						},
						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: shortInterval,
								MaxInterval:  longInterval,
							},
						},
						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)
}
