77import logging
88import json
99import base64
10+ import os
1011import urllib .parse
1112
13+ import requests
14+
1215log = logging .getLogger ()
1316
1417
1518class 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