Skip to content

Conversation

@morningman
Copy link
Contributor

@morningman morningman commented Jan 23, 2026

What problem does this PR solve?

Enhance the validatePlainPassword function in MysqlPassword.java to fully comply with MySQL's STRONG password validation policy.

Changes:

  1. Require all 4 character types (digit, lowercase, uppercase, special character) instead of the previous "3 out of 4" requirement.

  2. Add dictionary word check to reject passwords containing common weak words.

    • Built-in dictionary includes common words like: password, admin, test, root, etc.
    • Support loading custom dictionary from external file via the new global variable validate_password_dictionary_file.
  3. Implement lazy loading for external dictionary file:

    • Dictionary is loaded on first password validation call.
    • Automatically reloads when the file path is changed.
    • Falls back to built-in dictionary if file loading fails.
  4. Improve error messages to clearly indicate which requirements are missing.

  5. Add comprehensive unit tests for all validation scenarios.

Change the password dictionary file path resolution to use Config.security_plugins_dir
as the base directory prefix. New GlobalVariable.validatePasswordDictionaryFile only
needs to specify the filename, and the full path will be constructed as:
${security_plugins_dir}/<filename>

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

…TRONG policy

Enhance the validatePlainPassword function in MysqlPassword.java to fully comply
with MySQL's STRONG password validation policy.

Changes:
1. Require all 4 character types (digit, lowercase, uppercase, special character)
   instead of the previous "3 out of 4" requirement.

2. Add dictionary word check to reject passwords containing common weak words.
   - Built-in dictionary includes common words like: password, admin, test, root, etc.
   - Support loading custom dictionary from external file via the new global variable
     `validate_password_dictionary_file`.

3. Implement lazy loading for external dictionary file:
   - Dictionary is loaded on first password validation call.
   - Automatically reloads when the file path is changed.
   - Falls back to built-in dictionary if file loading fails.

4. Improve error messages to clearly indicate which requirements are missing.

5. Add comprehensive unit tests for all validation scenarios.

New global variable:
- `validate_password_dictionary_file`: Path to custom dictionary file (one word per line).
@Thearas
Copy link
Contributor

Thearas commented Jan 23, 2026

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman
Copy link
Contributor Author

run buildall

@doris-robot
Copy link

TPC-H: Total hot run time: 31426 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit d71c47e8cdc7f66928ea5f0cadcd1f580938b46a, data reload: false

------ Round 1 ----------------------------------
q1	17654	4809	4583	4583
q2	2057	309	192	192
q3	10245	1291	782	782
q4	10197	761	294	294
q5	7512	1982	1918	1918
q6	196	169	141	141
q7	867	680	587	587
q8	9287	1380	1095	1095
q9	4957	4558	4731	4558
q10	6766	1671	1271	1271
q11	513	284	284	284
q12	335	371	220	220
q13	17789	3828	3052	3052
q14	228	246	213	213
q15	594	525	512	512
q16	613	656	582	582
q17	661	742	524	524
q18	6706	6554	6731	6554
q19	1288	1116	608	608
q20	429	362	252	252
q21	3052	2183	2210	2183
q22	1117	1114	1021	1021
Total cold run time: 103063 ms
Total hot run time: 31426 ms

----- Round 2, with runtime_filter_mode=off -----
q1	5031	5097	5186	5097
q2	308	393	312	312
q3	2378	2874	2462	2462
q4	1654	1807	1391	1391
q5	4479	4418	4319	4319
q6	210	175	128	128
q7	2065	1877	1814	1814
q8	2617	2343	2435	2343
q9	7399	7104	7090	7090
q10	2567	2823	2350	2350
q11	562	503	466	466
q12	694	757	630	630
q13	3605	4093	3426	3426
q14	287	305	272	272
q15	536	503	506	503
q16	605	659	608	608
q17	1085	1327	1281	1281
q18	7476	7388	7346	7346
q19	796	779	807	779
q20	1868	1967	1849	1849
q21	4465	4122	4183	4122
q22	1111	1046	989	989
Total cold run time: 51798 ms
Total hot run time: 49577 ms

@doris-robot
Copy link

TPC-DS: Total hot run time: 172454 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit d71c47e8cdc7f66928ea5f0cadcd1f580938b46a, data reload: false

query5	5095	620	491	491
query6	330	213	193	193
query7	4231	449	267	267
query8	336	249	235	235
query9	8734	2898	2831	2831
query10	431	323	285	285
query11	15310	15166	14739	14739
query12	185	113	112	112
query13	1228	436	362	362
query14	6407	3072	2780	2780
query14_1	2636	2598	2634	2598
query15	199	190	168	168
query16	973	506	455	455
query17	1058	686	584	584
query18	2682	430	332	332
query19	208	178	162	162
query20	124	120	118	118
query21	214	138	123	123
query22	4373	4295	3963	3963
query23	16157	15830	15439	15439
query23_1	15491	15563	15338	15338
query24	7137	1539	1180	1180
query24_1	1181	1188	1186	1186
query25	544	450	404	404
query26	1241	279	153	153
query27	2746	439	273	273
query28	4570	2172	2159	2159
query29	816	551	479	479
query30	314	241	207	207
query31	818	629	552	552
query32	90	74	72	72
query33	521	362	307	307
query34	897	898	525	525
query35	727	771	675	675
query36	884	880	822	822
query37	139	101	91	91
query38	2706	2733	2679	2679
query39	787	749	725	725
query39_1	721	732	719	719
query40	216	134	120	120
query41	71	69	67	67
query42	96	92	96	92
query43	430	475	435	435
query44	1320	746	758	746
query45	192	189	179	179
query46	833	945	586	586
query47	1390	1452	1342	1342
query48	324	325	260	260
query49	618	438	370	370
query50	687	275	230	230
query51	3770	3807	3773	3773
query52	95	99	92	92
query53	219	233	169	169
query54	285	276	278	276
query55	83	79	76	76
query56	305	305	322	305
query57	1046	990	896	896
query58	280	275	268	268
query59	2074	2096	2020	2020
query60	344	334	324	324
query61	193	141	145	141
query62	389	366	309	309
query63	195	166	157	157
query64	4905	1140	822	822
query65	3850	3742	3766	3742
query66	1382	417	318	318
query67	15587	15546	15428	15428
query68	2629	1127	709	709
query69	418	307	292	292
query70	953	935	918	918
query71	309	286	270	270
query72	5371	3139	3220	3139
query73	593	715	320	320
query74	8819	8794	8541	8541
query75	2292	2332	1885	1885
query76	2411	1040	639	639
query77	354	397	308	308
query78	9605	9861	9143	9143
query79	1047	925	596	596
query80	1286	514	434	434
query81	542	268	230	230
query82	975	154	119	119
query83	331	270	247	247
query84	255	128	95	95
query85	882	500	407	407
query86	403	293	290	290
query87	2899	2837	2792	2792
query88	3483	2591	2558	2558
query89	306	247	242	242
query90	1939	166	169	166
query91	164	160	133	133
query92	79	74	70	70
query93	1112	1005	650	650
query94	657	329	257	257
query95	588	334	373	334
query96	634	498	232	232
query97	2320	2385	2282	2282
query98	210	203	198	198
query99	600	566	504	504
Total cold run time: 247972 ms
Total hot run time: 172454 ms

@doris-robot
Copy link

ClickBench: Total hot run time: 26.77 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit d71c47e8cdc7f66928ea5f0cadcd1f580938b46a, data reload: false

query1	0.06	0.05	0.05
query2	0.10	0.04	0.04
query3	0.26	0.09	0.09
query4	1.60	0.12	0.11
query5	0.28	0.25	0.26
query6	1.14	0.65	0.66
query7	0.04	0.03	0.03
query8	0.05	0.05	0.04
query9	0.57	0.49	0.49
query10	0.55	0.55	0.54
query11	0.15	0.10	0.10
query12	0.15	0.11	0.11
query13	0.60	0.58	0.59
query14	0.95	0.94	0.92
query15	0.79	0.78	0.79
query16	0.39	0.39	0.40
query17	1.06	1.08	1.05
query18	0.23	0.21	0.21
query19	1.86	1.80	1.87
query20	0.01	0.01	0.01
query21	15.45	0.25	0.13
query22	5.18	0.05	0.04
query23	16.03	0.28	0.10
query24	0.92	0.66	0.32
query25	0.09	0.05	0.08
query26	0.14	0.13	0.13
query27	0.06	0.06	0.05
query28	3.36	1.08	0.88
query29	12.53	3.89	3.18
query30	0.28	0.13	0.12
query31	2.82	0.65	0.39
query32	3.26	0.55	0.45
query33	3.00	3.00	3.15
query34	15.82	5.08	4.42
query35	4.43	4.45	4.51
query36	0.65	0.50	0.49
query37	0.11	0.06	0.07
query38	0.06	0.05	0.04
query39	0.04	0.03	0.02
query40	0.17	0.14	0.13
query41	0.09	0.03	0.02
query42	0.04	0.03	0.03
query43	0.06	0.03	0.03
Total cold run time: 95.43 s
Total hot run time: 26.77 s

@hello-stephen
Copy link
Contributor

FE UT Coverage Report

Increment line coverage 97.87% (46/47) 🎉
Increment coverage report
Complete coverage report

@hello-stephen
Copy link
Contributor

FE Regression Coverage Report

Increment line coverage 21.28% (10/47) 🎉
Increment coverage report
Complete coverage report

@morningman
Copy link
Contributor Author

run buildall

@doris-robot
Copy link

TPC-H: Total hot run time: 31278 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit c77d2bbc18701bee19866199887725e67c7e8073, data reload: false

------ Round 1 ----------------------------------
q1	17644	4781	4551	4551
q2	2012	303	188	188
q3	10261	1273	728	728
q4	10216	884	302	302
q5	7605	2090	1891	1891
q6	184	170	141	141
q7	867	714	582	582
q8	9255	1373	1083	1083
q9	4864	4631	4637	4631
q10	6771	1686	1253	1253
q11	521	311	268	268
q12	329	369	221	221
q13	17760	3781	3026	3026
q14	232	244	210	210
q15	605	522	522	522
q16	612	658	607	607
q17	652	747	542	542
q18	6612	6512	6641	6512
q19	1173	1091	690	690
q20	420	376	236	236
q21	2958	2266	1989	1989
q22	1148	1116	1105	1105
Total cold run time: 102701 ms
Total hot run time: 31278 ms

----- Round 2, with runtime_filter_mode=off -----
q1	5119	4930	4906	4906
q2	358	390	336	336
q3	2377	2871	2526	2526
q4	1441	1905	1434	1434
q5	4635	4320	4356	4320
q6	238	168	128	128
q7	1959	1931	1804	1804
q8	2547	2415	2453	2415
q9	7242	7188	7188	7188
q10	2520	2502	2121	2121
q11	527	451	429	429
q12	685	723	581	581
q13	3329	3815	3105	3105
q14	266	288	256	256
q15	532	496	491	491
q16	616	659	609	609
q17	1075	1311	1318	1311
q18	7473	7445	7095	7095
q19	828	757	798	757
q20	1884	1961	1857	1857
q21	4501	4199	4081	4081
q22	1038	1011	973	973
Total cold run time: 51190 ms
Total hot run time: 48723 ms

@doris-robot
Copy link

TPC-DS: Total hot run time: 172511 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit c77d2bbc18701bee19866199887725e67c7e8073, data reload: false

query5	4595	636	506	506
query6	344	217	195	195
query7	4217	457	261	261
query8	348	258	231	231
query9	8685	2832	2821	2821
query10	432	308	275	275
query11	15263	15098	14811	14811
query12	176	118	115	115
query13	1253	440	361	361
query14	6321	2989	2774	2774
query14_1	2683	2642	2636	2636
query15	190	187	169	169
query16	969	478	463	463
query17	1075	645	536	536
query18	2449	414	320	320
query19	188	178	144	144
query20	126	115	117	115
query21	207	135	122	122
query22	4071	4145	4240	4145
query23	16027	15486	15235	15235
query23_1	15466	15505	15429	15429
query24	7096	1507	1188	1188
query24_1	1159	1151	1167	1151
query25	508	434	381	381
query26	1229	249	147	147
query27	2799	457	273	273
query28	4583	2169	2119	2119
query29	760	525	423	423
query30	305	237	204	204
query31	786	632	560	560
query32	86	84	73	73
query33	526	364	323	323
query34	908	873	546	546
query35	721	767	703	703
query36	890	896	812	812
query37	140	105	87	87
query38	2815	2756	2700	2700
query39	778	761	711	711
query39_1	710	704	715	704
query40	228	133	122	122
query41	73	67	70	67
query42	101	101	93	93
query43	414	460	411	411
query44	1301	748	743	743
query45	194	186	181	181
query46	838	949	578	578
query47	1460	1519	1361	1361
query48	318	338	256	256
query49	610	442	356	356
query50	698	268	205	205
query51	3766	3781	3799	3781
query52	93	95	80	80
query53	215	220	173	173
query54	287	273	260	260
query55	87	82	76	76
query56	321	294	307	294
query57	1052	1070	926	926
query58	272	267	262	262
query59	2094	2143	2076	2076
query60	382	354	317	317
query61	170	173	165	165
query62	421	366	331	331
query63	198	164	161	161
query64	4996	1220	892	892
query65	3856	3736	3733	3733
query66	1440	414	313	313
query67	15483	15496	15508	15496
query68	2458	1054	745	745
query69	390	314	270	270
query70	943	929	939	929
query71	314	280	272	272
query72	5239	3117	3248	3117
query73	606	719	309	309
query74	8715	8758	8538	8538
query75	2271	2325	1867	1867
query76	2275	1046	655	655
query77	359	383	313	313
query78	9718	9834	9190	9190
query79	1077	885	583	583
query80	1293	512	433	433
query81	550	260	231	231
query82	1019	150	121	121
query83	322	257	237	237
query84	256	117	90	90
query85	884	472	402	402
query86	420	285	321	285
query87	2903	2859	2741	2741
query88	3519	2598	2565	2565
query89	308	254	248	248
query90	1987	170	163	163
query91	160	161	147	147
query92	74	69	73	69
query93	1104	1030	640	640
query94	651	325	302	302
query95	574	357	319	319
query96	644	502	230	230
query97	2391	2404	2330	2330
query98	211	204	204	204
query99	627	586	493	493
Total cold run time: 246296 ms
Total hot run time: 172511 ms

@doris-robot
Copy link

ClickBench: Total hot run time: 26.76 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit c77d2bbc18701bee19866199887725e67c7e8073, data reload: false

query1	0.05	0.04	0.05
query2	0.10	0.05	0.04
query3	0.25	0.08	0.08
query4	1.60	0.12	0.11
query5	0.28	0.26	0.26
query6	1.14	0.65	0.64
query7	0.03	0.02	0.03
query8	0.05	0.04	0.05
query9	0.56	0.50	0.49
query10	0.54	0.56	0.53
query11	0.14	0.10	0.10
query12	0.14	0.11	0.10
query13	0.59	0.58	0.59
query14	0.96	0.94	0.92
query15	0.79	0.78	0.78
query16	0.39	0.40	0.39
query17	1.07	1.10	0.99
query18	0.22	0.21	0.21
query19	1.91	1.81	1.83
query20	0.01	0.01	0.02
query21	15.43	0.25	0.14
query22	5.26	0.05	0.05
query23	15.84	0.26	0.10
query24	1.77	0.85	0.34
query25	0.09	0.09	0.08
query26	0.15	0.13	0.12
query27	0.12	0.05	0.05
query28	4.05	1.08	0.88
query29	12.55	3.94	3.16
query30	0.29	0.14	0.12
query31	2.81	0.62	0.39
query32	3.24	0.56	0.45
query33	3.06	3.06	3.09
query34	16.00	5.06	4.42
query35	4.44	4.42	4.45
query36	0.68	0.50	0.49
query37	0.11	0.07	0.06
query38	0.08	0.04	0.04
query39	0.05	0.03	0.04
query40	0.17	0.14	0.13
query41	0.09	0.04	0.03
query42	0.04	0.04	0.03
query43	0.04	0.03	0.04
Total cold run time: 97.18 s
Total hot run time: 26.76 s

@hello-stephen
Copy link
Contributor

FE UT Coverage Report

Increment line coverage 97.87% (46/47) 🎉
Increment coverage report
Complete coverage report

@hello-stephen
Copy link
Contributor

FE Regression Coverage Report

Increment line coverage 44.68% (21/47) 🎉
Increment coverage report
Complete coverage report

@morningman
Copy link
Contributor Author

run buildall

@morningman
Copy link
Contributor Author

run buildall

@doris-robot
Copy link

TPC-H: Total hot run time: 30967 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 631119378835313931c5d79611bb013ad525aa43, data reload: false

------ Round 1 ----------------------------------
q1	17660	4853	4576	4576
q2	2068	301	193	193
q3	10270	1275	742	742
q4	10195	790	305	305
q5	7548	2121	1770	1770
q6	190	170	142	142
q7	855	723	580	580
q8	9258	1405	1116	1116
q9	4825	4572	4515	4515
q10	6793	1677	1261	1261
q11	531	285	261	261
q12	337	375	225	225
q13	17777	3830	3058	3058
q14	233	244	222	222
q15	591	532	526	526
q16	618	659	579	579
q17	658	799	472	472
q18	6542	6390	6532	6390
q19	1299	1042	688	688
q20	454	384	245	245
q21	2971	2320	2023	2023
q22	1158	1094	1078	1078
Total cold run time: 102831 ms
Total hot run time: 30967 ms

----- Round 2, with runtime_filter_mode=off -----
q1	4976	4997	5024	4997
q2	346	423	329	329
q3	2432	2879	2431	2431
q4	1377	1882	1397	1397
q5	4588	4431	4330	4330
q6	216	170	133	133
q7	2082	1952	1873	1873
q8	2532	2415	2413	2413
q9	7167	7338	7047	7047
q10	2501	2676	2237	2237
q11	519	458	461	458
q12	664	697	559	559
q13	3290	3782	3064	3064
q14	265	296	257	257
q15	533	504	499	499
q16	618	654	629	629
q17	1143	1308	1384	1308
q18	7204	7237	7299	7237
q19	821	766	761	761
q20	1895	1963	1862	1862
q21	4445	4268	4048	4048
q22	1083	991	1006	991
Total cold run time: 50697 ms
Total hot run time: 48860 ms

@doris-robot
Copy link

TPC-DS: Total hot run time: 173576 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 631119378835313931c5d79611bb013ad525aa43, data reload: false

query5	4569	621	488	488
query6	325	218	205	205
query7	4213	457	263	263
query8	348	239	233	233
query9	8730	2869	2870	2869
query10	479	321	276	276
query11	15159	15171	14910	14910
query12	178	120	115	115
query13	1226	451	354	354
query14	6333	3142	2779	2779
query14_1	2646	2689	2646	2646
query15	208	189	174	174
query16	976	488	454	454
query17	1079	659	556	556
query18	2528	421	337	337
query19	200	180	155	155
query20	123	117	116	116
query21	214	132	120	120
query22	4164	4098	3887	3887
query23	15980	15549	15335	15335
query23_1	15450	15397	15289	15289
query24	7233	1530	1147	1147
query24_1	1141	1136	1156	1136
query25	518	437	391	391
query26	1244	256	150	150
query27	2778	444	276	276
query28	4591	2185	2178	2178
query29	777	525	433	433
query30	306	240	207	207
query31	832	612	565	565
query32	84	78	71	71
query33	521	351	305	305
query34	905	879	533	533
query35	721	758	684	684
query36	892	871	870	870
query37	136	102	87	87
query38	2758	2717	2666	2666
query39	794	775	732	732
query39_1	728	719	709	709
query40	225	140	122	122
query41	79	74	74	74
query42	106	99	100	99
query43	456	502	469	469
query44	1482	872	877	872
query45	204	195	187	187
query46	950	1029	639	639
query47	1472	1451	1465	1451
query48	378	384	275	275
query49	652	455	394	394
query50	717	278	215	215
query51	3741	3800	3812	3800
query52	95	95	88	88
query53	213	229	183	183
query54	315	275	266	266
query55	99	84	76	76
query56	329	324	321	321
query57	1071	1051	916	916
query58	284	278	283	278
query59	2249	2254	2082	2082
query60	366	371	347	347
query61	179	175	178	175
query62	413	367	333	333
query63	218	178	177	177
query64	5093	1225	975	975
query65	3868	3717	3765	3717
query66	1445	455	332	332
query67	15526	15645	15495	15495
query68	2392	1074	738	738
query69	400	313	288	288
query70	968	918	938	918
query71	311	293	275	275
query72	5404	3073	3253	3073
query73	612	737	332	332
query74	8812	8705	8516	8516
query75	2287	2331	1902	1902
query76	2279	1048	641	641
query77	365	385	305	305
query78	9895	9815	9304	9304
query79	3056	832	598	598
query80	1740	528	444	444
query81	576	266	232	232
query82	1016	153	123	123
query83	338	264	253	253
query84	263	124	96	96
query85	874	472	408	408
query86	487	304	288	288
query87	2862	2913	2798	2798
query88	3557	2651	2606	2606
query89	313	269	239	239
query90	1925	181	165	165
query91	166	159	140	140
query92	79	75	66	66
query93	1572	1055	663	663
query94	650	319	298	298
query95	572	337	398	337
query96	663	522	229	229
query97	2333	2386	2321	2321
query98	236	206	204	204
query99	598	593	529	529
Total cold run time: 250816 ms
Total hot run time: 173576 ms

@doris-robot
Copy link

ClickBench: Total hot run time: 26.89 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 631119378835313931c5d79611bb013ad525aa43, data reload: false

query1	0.05	0.04	0.04
query2	0.10	0.05	0.05
query3	0.27	0.09	0.08
query4	1.61	0.11	0.10
query5	0.27	0.26	0.25
query6	1.14	0.66	0.66
query7	0.03	0.02	0.03
query8	0.05	0.03	0.04
query9	0.58	0.52	0.49
query10	0.55	0.54	0.54
query11	0.14	0.09	0.10
query12	0.14	0.11	0.11
query13	0.60	0.58	0.60
query14	0.95	0.95	0.95
query15	0.79	0.78	0.79
query16	0.39	0.40	0.42
query17	1.07	1.01	1.04
query18	0.23	0.21	0.22
query19	2.00	1.88	1.84
query20	0.02	0.01	0.01
query21	15.44	0.27	0.15
query22	5.16	0.05	0.04
query23	15.93	0.29	0.10
query24	1.71	0.77	0.33
query25	0.12	0.05	0.07
query26	0.15	0.14	0.13
query27	0.07	0.07	0.06
query28	4.40	1.07	0.89
query29	12.55	3.97	3.20
query30	0.29	0.14	0.12
query31	2.82	0.64	0.40
query32	3.25	0.56	0.46
query33	2.97	3.01	3.07
query34	16.13	5.08	4.45
query35	4.42	4.47	4.41
query36	0.64	0.49	0.49
query37	0.11	0.06	0.07
query38	0.08	0.04	0.04
query39	0.04	0.04	0.03
query40	0.16	0.15	0.14
query41	0.09	0.03	0.03
query42	0.05	0.03	0.03
query43	0.05	0.04	0.03
Total cold run time: 97.61 s
Total hot run time: 26.89 s

@hello-stephen
Copy link
Contributor

FE UT Coverage Report

Increment line coverage 97.96% (48/49) 🎉
Increment coverage report
Complete coverage report

@morningman
Copy link
Contributor Author

run buildall

@doris-robot
Copy link

TPC-H: Total hot run time: 30675 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 276a2efa4be2e854445f2ecbf6a67e6d976a9ef0, data reload: false

------ Round 1 ----------------------------------
q1	17716	4835	4585	4585
q2	2006	301	193	193
q3	10280	1315	732	732
q4	10195	794	314	314
q5	7508	2108	1806	1806
q6	188	168	134	134
q7	881	717	578	578
q8	9270	1360	1016	1016
q9	4883	4587	4643	4587
q10	6727	1690	1318	1318
q11	527	293	276	276
q12	336	374	215	215
q13	17799	3812	3070	3070
q14	237	246	213	213
q15	593	524	521	521
q16	652	644	580	580
q17	653	736	535	535
q18	6751	6535	6404	6404
q19	1095	961	615	615
q20	385	349	226	226
q21	2645	2129	1807	1807
q22	1024	1028	950	950
Total cold run time: 102351 ms
Total hot run time: 30675 ms

----- Round 2, with runtime_filter_mode=off -----
q1	5070	4693	4691	4691
q2	330	398	315	315
q3	2137	2657	2321	2321
q4	1327	1748	1312	1312
q5	4078	4013	4004	4004
q6	209	171	130	130
q7	1905	1873	1726	1726
q8	2865	2513	2532	2513
q9	7238	7257	7170	7170
q10	2685	2729	2376	2376
q11	581	480	482	480
q12	736	805	666	666
q13	3637	4076	3486	3486
q14	279	313	275	275
q15	551	525	517	517
q16	682	667	638	638
q17	1159	1359	1365	1359
q18	8153	7961	7630	7630
q19	844	814	847	814
q20	2018	2158	1934	1934
q21	4650	4714	4438	4438
q22	1024	1012	991	991
Total cold run time: 52158 ms
Total hot run time: 49786 ms

@doris-robot
Copy link

TPC-DS: Total hot run time: 172314 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 276a2efa4be2e854445f2ecbf6a67e6d976a9ef0, data reload: false

query5	4899	603	494	494
query6	332	206	191	191
query7	4228	445	268	268
query8	338	239	239	239
query9	8709	2850	2854	2850
query10	487	322	277	277
query11	15253	15102	14803	14803
query12	179	119	129	119
query13	1270	500	391	391
query14	6358	3026	2763	2763
query14_1	2665	2636	2659	2636
query15	200	190	170	170
query16	990	484	456	456
query17	1071	667	574	574
query18	2578	439	328	328
query19	192	185	155	155
query20	137	115	112	112
query21	226	138	118	118
query22	3934	4137	3910	3910
query23	15882	15512	15512	15512
query23_1	15327	15523	15438	15438
query24	7280	1547	1138	1138
query24_1	1167	1163	1188	1163
query25	545	461	401	401
query26	1235	273	154	154
query27	2751	455	281	281
query28	4563	2184	2168	2168
query29	770	543	447	447
query30	317	249	202	202
query31	796	636	552	552
query32	86	79	72	72
query33	534	366	320	320
query34	923	890	541	541
query35	710	787	681	681
query36	881	912	824	824
query37	144	96	88	88
query38	2712	2646	2686	2646
query39	776	741	749	741
query39_1	719	736	697	697
query40	232	136	117	117
query41	72	72	68	68
query42	103	95	96	95
query43	478	433	429	429
query44	1340	752	752	752
query45	225	182	174	174
query46	830	935	568	568
query47	1442	1545	1347	1347
query48	308	329	250	250
query49	604	412	337	337
query50	671	270	219	219
query51	3764	3749	3698	3698
query52	91	93	85	85
query53	216	216	173	173
query54	269	256	248	248
query55	79	78	73	73
query56	308	289	292	289
query57	1050	1029	916	916
query58	270	252	253	252
query59	1958	2239	2150	2150
query60	321	320	315	315
query61	140	149	144	144
query62	391	356	310	310
query63	187	169	158	158
query64	4826	1116	829	829
query65	3805	3762	3733	3733
query66	1422	419	309	309
query67	15893	15518	15445	15445
query68	2379	1052	719	719
query69	409	314	271	271
query70	990	873	951	873
query71	296	329	268	268
query72	5212	3153	3235	3153
query73	616	722	308	308
query74	8746	8856	8539	8539
query75	2255	2319	1865	1865
query76	2289	1042	638	638
query77	352	376	293	293
query78	9687	10006	9075	9075
query79	1063	895	594	594
query80	1297	523	439	439
query81	528	260	222	222
query82	1724	149	120	120
query83	338	256	245	245
query84	251	111	94	94
query85	975	488	403	403
query86	462	309	294	294
query87	2833	2885	2749	2749
query88	3494	2592	2576	2576
query89	307	258	244	244
query90	1956	166	167	166
query91	172	167	134	134
query92	74	70	72	70
query93	1040	1036	656	656
query94	612	315	299	299
query95	575	324	362	324
query96	655	495	229	229
query97	2302	2337	2300	2300
query98	213	204	193	193
query99	598	566	548	548
Total cold run time: 247720 ms
Total hot run time: 172314 ms

@doris-robot
Copy link

ClickBench: Total hot run time: 27.62 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 276a2efa4be2e854445f2ecbf6a67e6d976a9ef0, data reload: false

query1	0.05	0.04	0.04
query2	0.10	0.05	0.05
query3	0.25	0.08	0.08
query4	1.61	0.12	0.12
query5	0.28	0.25	0.25
query6	1.14	0.64	0.64
query7	0.02	0.02	0.02
query8	0.06	0.04	0.04
query9	0.57	0.50	0.50
query10	0.55	0.56	0.55
query11	0.14	0.10	0.10
query12	0.14	0.10	0.10
query13	0.61	0.59	0.59
query14	0.96	0.93	0.94
query15	0.80	0.77	0.76
query16	0.40	0.39	0.40
query17	0.97	0.97	1.00
query18	0.22	0.21	0.21
query19	1.97	1.87	1.78
query20	0.02	0.01	0.01
query21	15.65	0.27	0.13
query22	5.26	0.05	0.04
query23	16.06	0.28	0.10
query24	1.03	1.35	1.18
query25	0.11	0.07	0.06
query26	0.15	0.13	0.13
query27	0.08	0.05	0.05
query28	5.27	1.06	0.87
query29	12.60	3.97	3.16
query30	0.27	0.13	0.12
query31	2.80	0.61	0.39
query32	3.23	0.54	0.46
query33	2.96	3.06	3.02
query34	16.10	5.05	4.48
query35	4.42	4.47	4.46
query36	0.67	0.49	0.48
query37	0.11	0.06	0.07
query38	0.07	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.14	0.13
query41	0.08	0.04	0.03
query42	0.05	0.03	0.03
query43	0.05	0.04	0.04
Total cold run time: 98.09 s
Total hot run time: 27.62 s

@hello-stephen
Copy link
Contributor

FE Regression Coverage Report

Increment line coverage 40.82% (20/49) 🎉
Increment coverage report
Complete coverage report

1 similar comment
@hello-stephen
Copy link
Contributor

FE Regression Coverage Report

Increment line coverage 40.82% (20/49) 🎉
Increment coverage report
Complete coverage report

@github-actions github-actions bot added the approved Indicates a PR has been approved by one committer. label Jan 27, 2026
@github-actions
Copy link
Contributor

PR approved by at least one committer and no changes requested.

@github-actions
Copy link
Contributor

PR approved by anyone and no changes requested.

@jgq2008303393
Copy link

LGTM

Copy link

@jgq2008303393 jgq2008303393 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances password validation in Apache Doris to align with MySQL's STRONG password policy. It introduces stricter character requirements (all 4 types required instead of 3 out of 4), adds dictionary-based weak password detection with both built-in and external dictionary support, and improves error messages for better user guidance.

Changes:

  • Enhanced password validation to require all 4 character types (digit, lowercase, uppercase, special character) and reject passwords containing common dictionary words
  • Added configurable external dictionary file support with lazy loading and automatic reload when the file path changes
  • Introduced new global variable validate_password_dictionary_file and Config property security_plugins_dir for dictionary file management

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlPassword.java Core implementation of enhanced password validation with dictionary checking, lazy loading, and improved error messages
fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java Added new global variable for dictionary file configuration and spacing improvements in Chinese descriptions
fe/fe-common/src/main/java/org/apache/doris/common/Config.java Added security_plugins_dir configuration property and spacing improvements in Chinese descriptions
fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlPasswordTest.java Comprehensive unit tests covering all validation scenarios including dictionary loading, reloading, and edge cases
regression-test/suites/account_p0/test_alter_user.groovy Updated test assertions to accommodate improved error messages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +152 to +154
loadedDictionaryWords = loadDictionaryFromFile(configuredFilePath);
loadedDictionaryFilePath = configuredFilePath;
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition in dictionary loading: The double-checked locking pattern is implemented, but there's a subtle issue. After loadedDictionaryWords is set to a non-null value on line 152, another thread could read loadedDictionaryWords (line 158) before loadedDictionaryFilePath is updated (line 153). This could cause the wrong dictionary to be returned briefly during a reload.

The assignment order should be reversed: first update loadedDictionaryFilePath, then loadedDictionaryWords. However, this would still have issues. A better approach is to assign both inside the synchronized block atomically, or use a holder object that contains both the path and words together to ensure they're always consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +416 to +417
// Should pass since dictionary effectively has no words
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG, "Test@123X");
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test testValidatePasswordDictionaryWithCommentsOnly passes a password "Test@123X" that contains the built-in dictionary word "test", and expects it to pass. However, looking at the implementation, when a dictionary file is successfully loaded (even if empty after filtering comments), it replaces the built-in dictionary. The issue is that if the loaded dictionary has no words (empty Set), it's still treated as a valid dictionary and returned. This means passwords like "Test@123X" would pass, which is inconsistent with the security expectations.

While this behavior might be intentional to allow administrators to disable dictionary checking by providing an empty file, it should be clearly documented. Consider whether an empty external dictionary should fall back to the built-in dictionary, or if it should truly disable dictionary checking.

Suggested change
// Should pass since dictionary effectively has no words
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG, "Test@123X");
// Even when the external dictionary file contains only comments,
// built-in dictionary words should still be enforced.
try {
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG, "Test@123X");
Assert.fail("Expected AnalysisException for test");
} catch (AnalysisException e) {
Assert.assertTrue(e.getMessage().contains("test"));
}

Copilot uses AI. Check for mistakes.
Comment on lines +447 to +454
private static String containsDictionaryWord(String password) {
String lowerPassword = password.toLowerCase();
for (String word : getDictionaryWords()) {
if (lowerPassword.contains(word)) {
return word;
}
}
return null;
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: The containsDictionaryWord method uses a simple substring search (contains()) for each dictionary word. For large dictionaries and frequent password validations, this could become a performance bottleneck as it results in O(n*m) complexity where n is the number of dictionary words and m is the password length.

Consider using a more efficient algorithm such as the Aho-Corasick algorithm for multi-pattern string matching, or pre-processing the dictionary into a trie structure. Alternatively, document the expected dictionary size limits to set appropriate performance expectations.

Copilot uses AI. Check for mistakes.
Comment on lines +309 to +310
// Password not containing custom dictionary words should pass
// Note: built-in words like "test" should NOT fail because we're using external dictionary
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design consideration: When an external dictionary file is successfully loaded, it completely replaces the built-in dictionary rather than supplementing it. This means passwords containing common weak words like "password", "admin", "test" will be allowed if they're not in the custom dictionary. This could weaken security if administrators provide incomplete custom dictionaries.

Consider whether the external dictionary should supplement (merge with) the built-in dictionary rather than replace it, or clearly document this behavior so administrators understand they need to include common weak words in their custom dictionaries for comprehensive protection.

Suggested change
// Password not containing custom dictionary words should pass
// Note: built-in words like "test" should NOT fail because we're using external dictionary
// Password containing built-in dictionary word should still fail even with external dictionary
try {
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG, "Test@123Xy");
Assert.fail("Expected AnalysisException for built-in dictionary word");
} catch (AnalysisException e) {
Assert.assertTrue(e.getMessage().contains("dictionary word"));
}
// Password not containing either built-in or custom dictionary words should pass

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +56
@After
public void tearDown() {
// Restore original values
GlobalVariable.validatePasswordDictionaryFile = originalDictionaryFile;
Config.security_plugins_dir = originalSecurityPluginsDir;
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test isolation issue: The tests modify global static state (loadedDictionaryWords and loadedDictionaryFilePath in MysqlPassword.java) but the tearDown method doesn't reset these cached values. This means the execution order of tests can affect their results. For example, if testValidatePasswordWithExternalDictionary runs before testValidatePasswordBuiltinDictionaryWord, the cached dictionary from the first test could interfere with the second test.

Consider adding a method to reset the dictionary cache (e.g., a package-private resetDictionaryCache() method in MysqlPassword) and call it in the setUp() or tearDown() methods to ensure test isolation.

Copilot uses AI. Check for mistakes.
Comment on lines +378 to +398
public void testValidatePasswordEmptyDictionaryFile() throws IOException, AnalysisException {
// Set security_plugins_dir to temp folder
Config.security_plugins_dir = tempFolder.getRoot().getAbsolutePath();

// Use just the filename
GlobalVariable.validatePasswordDictionaryFile = "empty_dict.txt";

// With empty dictionary, only character requirements should be checked
try {
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG, "Test@123X");
Assert.fail("Expected AnalysisException for test");
} catch (AnalysisException e) {
Assert.assertTrue(e.getMessage().contains("test"));
}
try {
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG, "Admin@12X");
Assert.fail("Expected AnalysisException for admin");
} catch (AnalysisException e) {
Assert.assertTrue(e.getMessage().contains("admin"));
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The testValidatePasswordEmptyDictionaryFile test expects passwords containing dictionary words ("test", "admin") to fail even when using an empty external dictionary file. However, the test does not actually create the empty_dict.txt file. When the file doesn't exist, loadDictionaryFromFile returns null, and getDictionaryWords falls back to BUILTIN_DICTIONARY_WORDS, which contains "test" and "admin". This makes the test pass for the wrong reason - it's testing the fallback behavior rather than the empty dictionary behavior.

The test should create the empty file to properly test the scenario where an empty external dictionary is loaded successfully. Add a line to create the empty file before setting the GlobalVariable, for example:
tempFolder.newFile("empty_dict.txt");

Copilot uses AI. Check for mistakes.
if (Strings.isNullOrEmpty(configuredFileName)) {
return BUILTIN_DICTIONARY_WORDS;
}

Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal vulnerability: The code does not validate the configuredFileName before constructing the file path. A malicious user with permission to set the validate_password_dictionary_file global variable could use path traversal sequences (e.g., "../../etc/passwd") to read arbitrary files from the filesystem. While normalize() is called, it's only used for comparison and doesn't prevent the initial file access.

Add validation to reject filenames containing path separators or traversal sequences. For example, check that configuredFileName doesn't contain '/', '', or '..' before constructing the path. This is similar to the security check in GetLogFileAction.java:129 which validates file names with a regex pattern.

Suggested change
// Validate that the configured file name is a simple file name without path separators
// or traversal sequences to prevent path traversal attacks.
if (configuredFileName.contains("/") || configuredFileName.contains("\\") || configuredFileName.contains("..")) {
LOG.warn("Invalid validate_password_dictionary_file '{}': path separators or traversal sequences are not allowed. "
+ "Falling back to built-in dictionary words.", configuredFileName);
return BUILTIN_DICTIONARY_WORDS;
}

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +152
@VariableMgr.VarAttr(name = VALIDATE_PASSWORD_DICTIONARY_FILE, flag = VariableMgr.GLOBAL,
description = {"密码验证字典文件路径。文件为纯文本格式,每行一个词。"
+ "当 validate_password_policy 为 STRONG(2) 时,密码中不能包含字典中的任何词(不区分大小写)。"
+ "如果为空,则使用内置字典。",
"Path to the password validation dictionary file. "
+ "The file should be plain text with one word per line. "
+ "When validate_password_policy is STRONG(2), "
+ "the password cannot contain any word from the dictionary "
+ "(case-insensitive). If empty, a built-in dictionary will be used."})
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description states "Path to the password validation dictionary file" and "If empty, a built-in dictionary will be used." However, based on the implementation (MysqlPassword.java:144), the value should be just a filename, not a full path. The full path is constructed by combining Config.security_plugins_dir with the provided filename. The description is misleading and should clearly state that only the filename should be specified, and it will be looked up in the security_plugins_dir directory.

Update the description to: "Filename of the password validation dictionary file in the security_plugins_dir. The file should be plain text with one word per line. When validate_password_policy is STRONG(2), the password cannot contain any word from the dictionary (case-insensitive). If empty, a built-in dictionary will be used."

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +139
// If no file is configured, use built-in dictionary
if (Strings.isNullOrEmpty(configuredFileName)) {
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing cache invalidation: When GlobalVariable.validatePasswordDictionaryFile is changed back to an empty string (to use built-in dictionary), the cached loadedDictionaryWords and loadedDictionaryFilePath are not cleared. The next call to getDictionaryWords() will correctly return BUILTIN_DICTIONARY_WORDS due to the early return on line 140, but the stale cached values remain in memory. If the variable is later set to the same filename again, the stale cache will be reused without checking if the file content has changed.

Add logic to clear the cache (set loadedDictionaryWords and loadedDictionaryFilePath to null) when configuredFileName is empty or when we detect a change.

Suggested change
// If no file is configured, use built-in dictionary
if (Strings.isNullOrEmpty(configuredFileName)) {
// If no file is configured, clear any cached external dictionary and use built-in dictionary
if (Strings.isNullOrEmpty(configuredFileName)) {
synchronized (DICTIONARY_LOAD_LOCK) {
loadedDictionaryWords = null;
loadedDictionaryFilePath = null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +156
// Check if we need to (re)load the dictionary
// Double-checked locking for thread safety
if (loadedDictionaryWords == null || !configuredFilePath.equals(loadedDictionaryFilePath)) {
synchronized (DICTIONARY_LOAD_LOCK) {
if (loadedDictionaryWords == null || !configuredFilePath.equals(loadedDictionaryFilePath)) {
loadedDictionaryWords = loadDictionaryFromFile(configuredFilePath);
loadedDictionaryFilePath = configuredFilePath;
}
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with file content change detection: The dictionary loading mechanism only detects when the file path changes, not when the file content changes. If an administrator updates the dictionary file content without changing the filename, the stale cached dictionary will continue to be used until the FE process is restarted or the filename is changed.

Consider adding file modification time tracking (using Files.getLastModifiedTime()) to detect when the file has been updated, and reload it automatically. Alternatively, document this limitation so administrators know they need to change the filename or restart the FE to reload the dictionary.

Copilot uses AI. Check for mistakes.
@morningman morningman merged commit 4291de9 into apache:master Jan 28, 2026
37 of 39 checks passed
github-actions bot pushed a commit that referenced this pull request Jan 28, 2026
…TRONG policy (#60188)

### What problem does this PR solve?

Enhance the validatePlainPassword function in MysqlPassword.java to
fully comply with MySQL's STRONG password validation policy.

Changes:
1. Require all 4 character types (digit, lowercase, uppercase, special
character) instead of the previous "3 out of 4" requirement.

2. Add dictionary word check to reject passwords containing common weak
words.
- Built-in dictionary includes common words like: password, admin, test,
root, etc.
- Support loading custom dictionary from external file via the new
global variable `validate_password_dictionary_file`.

3. Implement lazy loading for external dictionary file:
   - Dictionary is loaded on first password validation call.
   - Automatically reloads when the file path is changed.
   - Falls back to built-in dictionary if file loading fails.

4. Improve error messages to clearly indicate which requirements are
missing.

5. Add comprehensive unit tests for all validation scenarios.

Change the password dictionary file path resolution to use
`Config.security_plugins_dir`
as the base directory prefix. New
`GlobalVariable.validatePasswordDictionaryFile` only
needs to specify the filename, and the full path will be constructed as:
`${security_plugins_dir}/<filename>`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants