Skip to content

Commit b0cbd05

Browse files
Add Keycloak IDP tests for Eventing JIT provisioning guardrails
Introduce three tests that validate Eventing guardrail behavior when JWT authentication uses JIT provisioning with an external Keycloak IDP via JWKS URI. The block tests assert that function creation and deletion are rejected with ERR_JWT_JIT_NOT_SUPPORTED. The lifecycle test confirms deploy, pause, resume, and undeploy succeed using a Keycloak-issued access token. Two new helpers configure_jwt_with_jwks_uri and get_jwt_token_from_idp centralise Keycloak token acquisition and JWT setup. Used Factory Droid for code generation. Model used: Claude Sonnet 4.6. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Change-Id: I46d95c62137e07cd548843c17d1e2b910909ebe8 Reviewed-on: https://review.couchbase.org/c/testrunner/+/243858 Tested-by: Saimirra R <saimirra.r@couchbase.com> Reviewed-by: Bharath G P <bharath.gp@couchbase.com>
1 parent ace9df9 commit b0cbd05

2 files changed

Lines changed: 179 additions & 7 deletions

File tree

conf/eventing/eventing_jwt_auth.conf

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,23 @@ eventing.eventing_jwt_auth.EventingJWTAuth:
5151
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=timers
5252
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=base64
5353
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=crc
54-
test_jwt_with_diff_handler_codes,nodes_init=3,services_init=kv-n1ql:index-eventing:cbas,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=analytics_and_counters,GROUP=jwt_new_tests
55-
test_jwt_with_diff_handler_codes,nodes_init=3,services_init=kv-n1ql:index-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=n1ql,GROUP=jwt_new_tests
54+
test_jwt_with_diff_handler_codes,nodes_init=3,services_init=kv-n1ql:index-eventing:cbas,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=analytics_and_counters
55+
test_jwt_with_diff_handler_codes,nodes_init=3,services_init=kv-n1ql:index-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=n1ql
5656
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=bucket_cache
5757
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=subdoc
58-
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=xattrs,GROUP=jwt_new_tests
59-
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=sbm,GROUP=jwt_new_tests
58+
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=xattrs
59+
test_jwt_with_diff_handler_codes,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,use_single_bucket=True,jit_provisioning=False,handler_code=sbm
60+
61+
# JWKS URI Tests using Keycloak
62+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_create,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=admin@localhost.com,jwt_roles=admin,jwt_group=admin_group,GROUP=jwks_uri_keycloak
63+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_delete,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=admin@localhost.com,jwt_roles=admin,jwt_group=admin_group,GROUP=jwks_uri_keycloak
64+
test_eventing_jwt_auth_with_jwks_uri_keycloak_permit_lifecycle_ops,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=admin@localhost.com,jwt_roles=admin,jwt_group=admin_group,GROUP=jwks_uri_keycloak
65+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_create,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=eventing_admin@localhost.com,jwt_roles=eventing_admin,jwt_group=eventing_admin_group,GROUP=jwks_uri_keycloak
66+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_delete,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=eventing_admin@localhost.com,jwt_roles=eventing_admin,jwt_group=eventing_admin_group,GROUP=jwks_uri_keycloak
67+
test_eventing_jwt_auth_with_jwks_uri_keycloak_permit_lifecycle_ops,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=eventing_admin@localhost.com,jwt_roles=eventing_admin,jwt_group=eventing_admin_group,GROUP=jwks_uri_keycloak
68+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_create,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=eventing_scoped@localhost.com,jwt_roles=eventing_manage_functions[src_bucket:_default],data_dcp_reader[src_bucket:_default:_default],data_reader[metadata:_default:_default],data_writer[metadata:_default:_default],jwt_group=eventing_scoped_group,GROUP=jwks_uri_keycloak
69+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_delete,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=eventing_scoped@localhost.com,jwt_roles=eventing_manage_functions[src_bucket:_default],data_dcp_reader[src_bucket:_default:_default],data_reader[metadata:_default:_default],data_writer[metadata:_default:_default],jwt_group=eventing_scoped_group,GROUP=jwks_uri_keycloak
70+
test_eventing_jwt_auth_with_jwks_uri_keycloak_permit_lifecycle_ops,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=eventing_scoped@localhost.com,jwt_roles=eventing_manage_functions[src_bucket:_default],data_dcp_reader[src_bucket:_default:_default],data_reader[metadata:_default:_default],data_writer[metadata:_default:_default],jwt_group=eventing_scoped_group,GROUP=jwks_uri_keycloak
71+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_create,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=combined_user@localhost.com,jwt_roles=data_dcp_reader[src_bucket:_default:_default],data_reader[metadata:_default:_default],data_writer[metadata:_default:_default],jwt_group=combined_user_group,GROUP=jwks_uri_keycloak
72+
test_eventing_jwt_auth_with_jwks_uri_keycloak_block_delete,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=combined_user@localhost.com,jwt_roles=data_dcp_reader[src_bucket:_default:_default],data_reader[metadata:_default:_default],data_writer[metadata:_default:_default],jwt_group=combined_user_group,GROUP=jwks_uri_keycloak
73+
test_eventing_jwt_auth_with_jwks_uri_keycloak_permit_lifecycle_ops,nodes_init=2,services_init=kv-eventing,dataset=default,groups=simple,reset_services=True,jit_provisioning=True,handler_code=bucket_op,keycloak_username=combined_user@localhost.com,jwt_roles=data_dcp_reader[src_bucket:_default:_default],data_reader[metadata:_default:_default],data_writer[metadata:_default:_default],jwt_group=combined_user_group,GROUP=jwks_uri_keycloak

pytests/eventing/eventing_jwt_auth.py

Lines changed: 161 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
import logging
88
import json
99
import base64
10+
import os
1011
import urllib.parse
1112

13+
import requests
14+
1215
log = logging.getLogger()
1316

1417

1518
class EventingJWTAuth(EventingBaseTest):
1619
def setUp(self):
1720
super(EventingJWTAuth, self).setUp()
18-
1921
# JWT Configuration
2022
self.jwt_algorithm = self.input.param('jwt_algorithm', 'ES256')
2123
self.jwt_issuer = self.input.param('jwt_issuer', 'custom-issuer')
@@ -27,7 +29,17 @@ def setUp(self):
2729
self.jwt_ttl = self.input.param('jwt_ttl', 3600)
2830
self.logsize = self.input.param("size", None)
2931
self.aggregate= self.input.param("aggregate", False)
30-
32+
# Keycloak IDP Config
33+
self.keycloak_ip = os.environ.get('KEYCLOAK_IDP_IP', '')
34+
self.keycloak_port = self.input.param('keycloak_port', 8444)
35+
self.keycloak_realm = self.input.param('keycloak_realm', 'cb')
36+
self.keycloak_client_id = self.input.param('keycloak_client_id', 'test-client')
37+
self.keycloak_client_secret = os.environ.get('KEYCLOAK_IDP_CLIENT_SECRET', '')
38+
self.keycloak_username = self.input.param('keycloak_username', 'admin@localhost.com')
39+
self.keycloak_password = self.input.param('keycloak_password', 'password')
40+
self.keycloak_algorithm = self.input.param('keycloak_algorithm', 'RS384')
41+
self.roles_claim = self.input.param('roles_claim', 'resource_access.test-client.roles')
42+
self.jwks_uri_tls_verify = self.input.param('jwks_uri_tls_verify', False)
3143

3244
# Initialize JWT utilities
3345
self.jwt_utils = JWTUtils(log=self.log)
@@ -149,6 +161,67 @@ def configure_jwt_on_cluster(self, public_key):
149161
raise Exception(f"Failed to configure JWT: {content}")
150162
log.info("JWT configured successfully")
151163

164+
def configure_jwt_with_jwks_uri(self):
165+
"""
166+
Configure JWT on the cluster using an external IDP JWKS URI
167+
"""
168+
issuer_name = f"https://{self.keycloak_ip}:{self.keycloak_port}/realms/{self.keycloak_realm}"
169+
jwks_uri = f"https://{self.keycloak_ip}:{self.keycloak_port}/realms/{self.keycloak_realm}/protocol/openid-connect/certs"
170+
jwt_config = {
171+
"enabled": True,
172+
"issuers": [
173+
{
174+
"name": issuer_name,
175+
"signingAlgorithm": self.keycloak_algorithm,
176+
"audClaim": "azp",
177+
"audienceHandling": "any",
178+
"audiences": [self.keycloak_client_id],
179+
"subClaim": "preferred_username",
180+
"publicKeySource": "jwks_uri",
181+
"jwksUri": jwks_uri,
182+
"jwksUriTlsVerifyPeer": self.jwks_uri_tls_verify,
183+
"jitProvisioning": True,
184+
"rolesClaim": self.roles_claim
185+
}
186+
]
187+
}
188+
log.info(f"Configuring JWT with issuer '{issuer_name}', JWKS URI '{jwks_uri}'")
189+
status, content, _ = self.rest.create_jwt_with_config(jwt_config)
190+
if not status:
191+
raise Exception(f"Failed to configure JWT with JWKS URI: {content}")
192+
log.info("JWT configured with JWKS URI successfully")
193+
194+
def get_jwt_token_from_idp(self):
195+
"""
196+
Obtain a JWT access token from Keycloak via OAuth2 password grant
197+
"""
198+
token_endpoint = f"https://{self.keycloak_ip}:{self.keycloak_port}/realms/{self.keycloak_realm}/protocol/openid-connect/token"
199+
log.info(f"Requesting JWT token from Keycloak: {token_endpoint}")
200+
data = {
201+
"grant_type": "password",
202+
"scope": "openid",
203+
"client_id": self.keycloak_client_id,
204+
"client_secret": self.keycloak_client_secret,
205+
"username": self.keycloak_username,
206+
"password": self.keycloak_password
207+
}
208+
response = requests.post(
209+
token_endpoint,
210+
data=data,
211+
headers={"Content-Type": "application/x-www-form-urlencoded"},
212+
timeout=30,
213+
verify=self.jwks_uri_tls_verify
214+
)
215+
if response.status_code != 200:
216+
raise Exception(
217+
f"Failed to obtain JWT token from IDP: {response.status_code} {response.text}"
218+
)
219+
access_token = response.json().get("access_token")
220+
if not access_token:
221+
raise Exception(f"No access_token in IDP response: {response.text}")
222+
log.info("JWT token obtained from IDP successfully")
223+
return access_token
224+
152225
def test_eventing_jwt_auth_sanity(self):
153226
'''
154227
Create and deploy eventing function with JWT authentication
@@ -539,4 +612,89 @@ def test_jwt_with_diff_handler_codes(self):
539612
self.undeploy_function(body, jwt_token=self.jwt_token)
540613

541614
log.info("Deleting eventing function with JWT authentication")
542-
self.delete_function(body, jwt_token=self.jwt_token)
615+
self.delete_function(body, jwt_token=self.jwt_token)
616+
617+
618+
def test_eventing_jwt_auth_with_jwks_uri_keycloak_block_create(self):
619+
log.info(f"Creating JWT group '{self.jwt_group}' with roles '{self.jwt_roles}'")
620+
self.create_jwt_group()
621+
log.info("Configuring JWT with JWKS URI (JIT provisioning enabled)")
622+
self.configure_jwt_with_jwks_uri()
623+
log.info("Obtaining JWT token from Keycloak IDP")
624+
jwt_token = self.get_jwt_token_from_idp()
625+
626+
# Create and deploy eventing function with IDP JWT
627+
log.info("Try creating eventing function with IDP JWT authentication")
628+
try:
629+
log.info("Try creating eventing function with IDP JWT authentication")
630+
_ = self.create_save_function_body(self.function_name, self.handler_code, jwt_token=jwt_token)
631+
except Exception as e:
632+
log.info(f"Creation correctly failed due to Eventing Guardrail for JWT: {e}")
633+
assert "ERR_JWT_JIT_NOT_SUPPORTED" in str(e) and "JWT authentication with JIT provisioning is not supported in Eventing" in str(e), True
634+
log.info("JWT Function Creation Guardrail Tested Successfully")
635+
636+
637+
def test_eventing_jwt_auth_with_jwks_uri_keycloak_block_delete(self):
638+
log.info(f"Creating JWT group '{self.jwt_group}' with roles '{self.jwt_roles}'")
639+
self.create_jwt_group()
640+
log.info("Configuring JWT with JWKS URI (JIT provisioning enabled)")
641+
self.configure_jwt_with_jwks_uri()
642+
log.info("Obtaining JWT token from Keycloak IDP")
643+
self.jwt_token = self.get_jwt_token_from_idp()
644+
645+
# Create and deploy eventing function using Basic Auth
646+
log.info("Creating an eventing function with Basic Auth as Creation is blocked")
647+
body = self.create_save_function_body(self.function_name, self.handler_code)
648+
649+
try:
650+
log.info("Try deleting eventing function with IDP JWT authentication")
651+
self.delete_function(body, jwt_token=self.jwt_token)
652+
except Exception as e:
653+
log.info(f"Deletion correctly failed due to Eventing Guardrail for JWT: {e}")
654+
assert "ERR_JWT_JIT_NOT_SUPPORTED" in str(
655+
e) and "JWT authentication with JIT provisioning is not supported in Eventing" in str(e), True
656+
log.info("JWT Function Deletion Guardrail Tested Successfully")
657+
658+
def test_eventing_jwt_auth_with_jwks_uri_keycloak_permit_lifecycle_ops(self):
659+
log.info(f"Creating JWT group '{self.jwt_group}' with roles '{self.jwt_roles}'")
660+
self.create_jwt_group()
661+
662+
log.info("Configuring JWT with JWKS URI (JIT provisioning enabled)")
663+
self.configure_jwt_with_jwks_uri()
664+
665+
log.info("Obtaining JWT token from Keycloak IDP")
666+
jwt_token = self.get_jwt_token_from_idp()
667+
668+
# Create and deploy eventing function using Basic Auth
669+
log.info("Creating an eventing function with Basic Auth as Creation is blocked")
670+
body = self.create_save_function_body(self.function_name, self.handler_code)
671+
672+
try:
673+
log.info("Deploying eventing function with IDP JWT authentication")
674+
self.deploy_function(body, jwt_token=jwt_token)
675+
676+
log.info("Loading data to source bucket")
677+
self.load_data_to_collection(self.docs_per_day * self.num_docs, "src_bucket._default._default")
678+
679+
log.info("Verifying mutations are processed")
680+
self.verify_doc_count_collections("dst_bucket._default._default", self.docs_per_day * self.num_docs)
681+
682+
log.info("Pausing eventing function with IDP JWT authentication")
683+
self.pause_function(body, jwt_token=jwt_token)
684+
685+
log.info("Resuming eventing function with IDP JWT authentication")
686+
self.resume_function(body, jwt_token=jwt_token)
687+
688+
self.load_data_to_collection(self.docs_per_day * self.num_docs, "src_bucket._default._default", is_delete=True)
689+
self.sleep(10)
690+
self.verify_doc_count_collections("dst_bucket._default._default", 0)
691+
692+
log.info("Undeploying eventing function with IDP JWT authentication")
693+
self.undeploy_function(body, jwt_token=jwt_token)
694+
except Exception as e:
695+
log.info(f"Lifecycle operations correctly failed with forbidden error: {e}")
696+
assert "ERR_FORBIDDEN" in str(e) and "Forbidden" in str(e), True
697+
log.info("Forbidden error test for IDP JWT lifecycle operations completed successfully")
698+
699+
log.info("Deleting eventing function with Basic Auth as Deletion is blocked")
700+
self.delete_function(body)

0 commit comments

Comments
 (0)