@@ -117,13 +117,22 @@ suite("test_tls_cert_san_auth") {
117117 // === Cleanup function ===
118118 def cleanup = {
119119 logger. info(" Cleaning up test users and restoring config..." )
120+ // MySQL protocol test users
120121 try_sql(" DROP USER IF EXISTS '${ testUserBase} _1'@'%'" )
121122 try_sql(" DROP USER IF EXISTS '${ testUserBase} _2'@'%'" )
122123 try_sql(" DROP USER IF EXISTS '${ testUserBase} _3'@'%'" )
123124 try_sql(" DROP USER IF EXISTS '${ testUserBase} _4'@'%'" )
124125 try_sql(" DROP USER IF EXISTS '${ testUserBase} _5'@'%'" )
125126 try_sql(" DROP USER IF EXISTS '${ testUserBase} _6'@'%'" )
126127 try_sql(" DROP USER IF EXISTS '${ testUserBase} _7'@'%'" )
128+ // HTTPS test users
129+ try_sql(" DROP USER IF EXISTS '${ testUserBase} _http1'@'%'" )
130+ try_sql(" DROP USER IF EXISTS '${ testUserBase} _http2'@'%'" )
131+ try_sql(" DROP USER IF EXISTS '${ testUserBase} _http3'@'%'" )
132+ try_sql(" DROP USER IF EXISTS '${ testUserBase} _http4'@'%'" )
133+ try_sql(" DROP USER IF EXISTS '${ testUserBase} _http5'@'%'" )
134+ try_sql(" DROP USER IF EXISTS '${ testUserBase} _http6'@'%'" )
135+ try_sql(" DROP USER IF EXISTS '${ testUserBase} _http7'@'%'" )
127136 try {
128137 sql " ADMIN SET FRONTEND CONFIG ('tls_cert_based_auth_ignore_password' = '${ origIgnorePassword} ')"
129138 } catch (Exception e) {
@@ -239,7 +248,187 @@ suite("test_tls_cert_san_auth") {
239248 assertTrue (result7c, " Test 7c should succeed: URI SAN match" )
240249 logger. info(" Test 7c PASSED: URI SAN match works" )
241250
242- logger. info(" === All TLS SAN authentication tests PASSED ===" )
251+ logger. info(" === All MySQL protocol TLS SAN authentication tests PASSED ===" )
252+
253+ // ==================================================================================
254+ // HTTPS/HTTP Certificate-Based Authentication Tests
255+ // These tests verify that FE's HTTP endpoints also support cert-based auth
256+ // ==================================================================================
257+ logger. info(" === Starting HTTPS certificate-based auth tests ===" )
258+
259+ // Get FE HTTP connection info
260+ def feHostIp = mysqlHost // Actual IP to connect to
261+ def httpPort = context. config. feHttpPort ?: " 8030"
262+ def httpEndpoint = " /api/bootstrap" // Simple endpoint that requires auth
263+
264+ logger. info(" FE HTTPS host IP: ${ feHostIp} , port: ${ httpPort} " )
265+ logger. info(" Test endpoint: ${ httpEndpoint} " )
266+
267+ // Helper: Execute curl command and check result
268+ def executeCurlCommand = { String command , boolean expectSuccess = true ->
269+ def cmds = [" /bin/bash" , " -c" , command]
270+ logger. info(" Execute curl: ${ command} " )
271+ Process p = cmds. execute()
272+ def errMsg = new StringBuilder ()
273+ def msg = new StringBuilder ()
274+ p. waitForProcessOutput(msg, errMsg)
275+
276+ def output = msg. toString(). trim()
277+ def error = errMsg. toString(). trim()
278+ logger. info(" stdout: ${ output} " )
279+ logger. info(" stderr: ${ error} " )
280+ logger. info(" exitValue: ${ p.exitValue()} " )
281+
282+ // For curl with -w '%{http_code}', success means HTTP 200
283+ // Authentication failure typically returns 401
284+ if (expectSuccess) {
285+ // Expect HTTP 200 (or 2xx)
286+ return output. startsWith(" 2" ) || output == " 200"
287+ } else {
288+ // Expect non-2xx (typically 401 Unauthorized)
289+ return ! output. startsWith(" 2" ) && output != " 200"
290+ }
291+ }
292+
293+ // Helper: Build curl command with SAN certificate
294+ // Use --resolve to map localhost to actual IP, avoiding SNI issues with IP addresses
295+ // Use -k to skip server certificate verification (we're testing client cert auth)
296+ def buildCurlWithCert = { String user , String password , String endpoint ->
297+ return " curl -s -k -o /dev/null -w '%{http_code}' " +
298+ " --resolve 'localhost:${ httpPort} :${ feHostIp} ' " +
299+ " -u '${ user} :${ password} ' " +
300+ " --cert ${ sanClientCert} --key ${ sanClientKey} " +
301+ " https://localhost:${ httpPort}${ endpoint} 2>&1"
302+ }
303+
304+ // Helper: Build curl command without client certificate
305+ def buildCurlNoCert = { String user , String password , String endpoint ->
306+ return " curl -s -k -o /dev/null -w '%{http_code}' " +
307+ " --resolve 'localhost:${ httpPort} :${ feHostIp} ' " +
308+ " -u '${ user} :${ password} ' " +
309+ " https://localhost:${ httpPort}${ endpoint} 2>&1"
310+ }
311+
312+ // Helper: Build curl command with no-SAN certificate
313+ def buildCurlWithNoSanCert = { String user , String password , String endpoint ->
314+ return " curl -s -k -o /dev/null -w '%{http_code}' " +
315+ " --resolve 'localhost:${ httpPort} :${ feHostIp} ' " +
316+ " -u '${ user} :${ password} ' " +
317+ " --cert ${ noSanClientCert} --key ${ noSanClientKey} " +
318+ " https://localhost:${ httpPort}${ endpoint} 2>&1"
319+ }
320+
321+ // Helper: Build curl command with mismatched SAN certificate (not currently used)
322+ def buildCurlWithMismatchCert = { String user , String password , String endpoint ->
323+ return " curl -s -k -o /dev/null -w '%{http_code}' " +
324+ " --resolve 'localhost:${ httpPort} :${ feHostIp} ' " +
325+ " -u '${ user} :${ password} ' " +
326+ " --cert ${ sanClientCert} --key ${ sanClientKey} " +
327+ " https://localhost:${ httpPort}${ endpoint} 2>&1"
328+ }
329+
330+ // Reset config for HTTPS tests
331+ sql " ADMIN SET FRONTEND CONFIG ('tls_cert_based_auth_ignore_password' = 'false')"
332+
333+ // === HTTP Test 1: REQUIRE SAN + matching cert + correct password -> success ===
334+ logger. info(" === HTTP Test 1: REQUIRE SAN + matching cert + correct password ===" )
335+ sql " CREATE USER '${ testUserBase} _http1'@'%' IDENTIFIED BY '${ testPassword} ' REQUIRE SAN '${ sanEmail} '"
336+ sql " GRANT ADMIN_PRIV ON *.*.* TO '${ testUserBase} _http1'@'%'"
337+
338+ def httpCmd1 = buildCurlWithCert(" ${ testUserBase} _http1" , testPassword, httpEndpoint)
339+ def httpResult1 = executeCurlCommand(httpCmd1, true )
340+ assertTrue (httpResult1, " HTTP Test 1 should succeed: matching SAN + correct password" )
341+ logger. info(" HTTP Test 1 PASSED: HTTPS request successful with matching SAN and password" )
342+
343+ // === HTTP Test 2: REQUIRE SAN + matching cert + wrong password -> failure ===
344+ logger. info(" === HTTP Test 2: REQUIRE SAN + matching cert + wrong password ===" )
345+ sql " CREATE USER '${ testUserBase} _http2'@'%' IDENTIFIED BY '${ testPassword} ' REQUIRE SAN '${ sanEmail} '"
346+ sql " GRANT ADMIN_PRIV ON *.*.* TO '${ testUserBase} _http2'@'%'"
347+
348+ def httpCmd2 = buildCurlWithCert(" ${ testUserBase} _http2" , " wrong_password" , httpEndpoint)
349+ def httpResult2 = executeCurlCommand(httpCmd2, false )
350+ assertTrue (httpResult2, " HTTP Test 2 should fail: wrong password even with matching SAN" )
351+ logger. info(" HTTP Test 2 PASSED: HTTPS request rejected with wrong password" )
352+
353+ // === HTTP Test 3: REQUIRE SAN + mismatched SAN cert -> failure ===
354+ logger. info(" === HTTP Test 3: REQUIRE SAN + mismatched SAN ===" )
355+ sql " CREATE USER '${ testUserBase} _http3'@'%' IDENTIFIED BY '${ testPassword} ' REQUIRE SAN '${ sanMismatch} '"
356+ sql " GRANT ADMIN_PRIV ON *.*.* TO '${ testUserBase} _http3'@'%'"
357+
358+ def httpCmd3 = buildCurlWithCert(" ${ testUserBase} _http3" , testPassword, httpEndpoint)
359+ def httpResult3 = executeCurlCommand(httpCmd3, false )
360+ assertTrue (httpResult3, " HTTP Test 3 should fail: SAN mismatch" )
361+ logger. info(" HTTP Test 3 PASSED: HTTPS request rejected with mismatched SAN" )
362+
363+ // === HTTP Test 4: REQUIRE SAN + no certificate -> failure ===
364+ // Note: This test depends on tls_verify_mode=verify_fail_if_no_peer_cert
365+ // If the server requires client cert, curl without --cert will fail at TLS handshake
366+ logger. info(" === HTTP Test 4: REQUIRE SAN + no certificate ===" )
367+ sql " CREATE USER '${ testUserBase} _http4'@'%' IDENTIFIED BY '${ testPassword} ' REQUIRE SAN '${ sanEmail} '"
368+ sql " GRANT ADMIN_PRIV ON *.*.* TO '${ testUserBase} _http4'@'%'"
369+
370+ def httpCmd4 = buildCurlNoCert(" ${ testUserBase} _http4" , testPassword, httpEndpoint)
371+ def httpResult4 = executeCurlCommand(httpCmd4, false )
372+ assertTrue (httpResult4, " HTTP Test 4 should fail: no certificate provided for user with REQUIRE SAN" )
373+ logger. info(" HTTP Test 4 PASSED: HTTPS request rejected without client certificate" )
374+
375+ // === HTTP Test 5: REQUIRE SAN + matching cert + ignore_password=true -> success ===
376+ logger. info(" === HTTP Test 5: REQUIRE SAN + ignore_password=true ===" )
377+ sql " ADMIN SET FRONTEND CONFIG ('tls_cert_based_auth_ignore_password' = 'true')"
378+ sql " CREATE USER '${ testUserBase} _http5'@'%' IDENTIFIED BY '${ testPassword} ' REQUIRE SAN '${ sanEmail} '"
379+ sql " GRANT ADMIN_PRIV ON *.*.* TO '${ testUserBase} _http5'@'%'"
380+
381+ // Use wrong password - should still succeed because ignore_password=true
382+ def httpCmd5 = buildCurlWithCert(" ${ testUserBase} _http5" , " any_wrong_password" , httpEndpoint)
383+ def httpResult5 = executeCurlCommand(httpCmd5, true )
384+ assertTrue (httpResult5, " HTTP Test 5 should succeed: ignore_password=true allows login with cert only" )
385+ logger. info(" HTTP Test 5 PASSED: HTTPS request successful with certificate only (password ignored)" )
386+
387+ // Reset config
388+ sql " ADMIN SET FRONTEND CONFIG ('tls_cert_based_auth_ignore_password' = 'false')"
389+
390+ // === HTTP Test 6: REQUIRE NONE + no-SAN cert + correct password -> success ===
391+ logger. info(" === HTTP Test 6: REQUIRE NONE + no-SAN cert ===" )
392+ sql " CREATE USER '${ testUserBase} _http6'@'%' IDENTIFIED BY '${ testPassword} '"
393+ sql " GRANT ADMIN_PRIV ON *.*.* TO '${ testUserBase} _http6'@'%'"
394+
395+ def httpCmd6 = buildCurlWithNoSanCert(" ${ testUserBase} _http6" , testPassword, httpEndpoint)
396+ def httpResult6 = executeCurlCommand(httpCmd6, true )
397+ assertTrue (httpResult6, " HTTP Test 6 should succeed: no TLS requirements, password auth works" )
398+ logger. info(" HTTP Test 6 PASSED: HTTPS request successful for user without TLS requirements" )
399+
400+ // === HTTP Test 7: ALTER USER add/remove REQUIRE SAN for HTTP ===
401+ logger. info(" === HTTP Test 7: ALTER USER add/remove REQUIRE SAN for HTTP ===" )
402+ sql " CREATE USER '${ testUserBase} _http7'@'%' IDENTIFIED BY '${ testPassword} ' REQUIRE SAN '${ sanEmail} '"
403+ sql " GRANT ADMIN_PRIV ON *.*.* TO '${ testUserBase} _http7'@'%'"
404+
405+ // First verify it works with matching cert
406+ def httpCmd7a = buildCurlWithCert(" ${ testUserBase} _http7" , testPassword, httpEndpoint)
407+ def httpResult7a = executeCurlCommand(httpCmd7a, true )
408+ assertTrue (httpResult7a, " HTTP Test 7a should succeed: initial REQUIRE SAN with matching cert" )
409+ logger. info(" HTTP Test 7a PASSED: REQUIRE SAN works with matching cert via HTTPS" )
410+
411+ // Remove REQUIRE SAN
412+ sql " ALTER USER '${ testUserBase} _http7'@'%' REQUIRE NONE"
413+
414+ // Now should work with no-SAN certificate
415+ def httpCmd7b = buildCurlWithNoSanCert(" ${ testUserBase} _http7" , testPassword, httpEndpoint)
416+ def httpResult7b = executeCurlCommand(httpCmd7b, true )
417+ assertTrue (httpResult7b, " HTTP Test 7b should succeed: REQUIRE NONE allows any cert" )
418+ logger. info(" HTTP Test 7b PASSED: REQUIRE NONE works with no-SAN certificate via HTTPS" )
419+
420+ // Add back REQUIRE SAN with different SAN type (DNS)
421+ sql " ALTER USER '${ testUserBase} _http7'@'%' REQUIRE SAN '${ sanDns} '"
422+
423+ // Now should work with DNS SAN from the same cert
424+ def httpCmd7c = buildCurlWithCert(" ${ testUserBase} _http7" , testPassword, httpEndpoint)
425+ def httpResult7c = executeCurlCommand(httpCmd7c, true )
426+ assertTrue (httpResult7c, " HTTP Test 7c should succeed: DNS SAN match" )
427+ logger. info(" HTTP Test 7c PASSED: REQUIRE SAN (DNS) works via HTTPS" )
428+
429+ logger. info(" === All HTTPS certificate-based auth tests PASSED ===" )
430+
431+ logger. info(" === All TLS SAN authentication tests (MySQL + HTTPS) PASSED ===" )
243432
244433 } finally {
245434 cleanup()
0 commit comments