91ca3b012c
* [NET-3090] Add new JWT provider config entry * Add initial test cases * update validations for jwt-provider config entry fields * more validation * start improving tests * more tests * Normalize * Improve tests and move validate fns * usage test update * Add split between ent and oss for partitions * fix lint issues * Added retry backoff, fixed tests, removed unused defaults * take into account default partitions * use countTrue and add aliases * omit audiences if empty * fix failing tests * add omit-entry * update copyright headers ids --------- Co-authored-by: Ronald Ekambi <ronekambi@gmail.com> Co-authored-by: Ronald <roncodingenthusiast@users.noreply.github.com>
370 lines
9.3 KiB
Go
370 lines
9.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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
|
|
|
|
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 - 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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
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: 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)
|
|
}
|