@@ -571,7 +571,104 @@ static int test_MatchUnsupportedSelector(void)
571571 }
572572 wolfSSHD_ConfigFree (head );
573573 }
574+ return ret ;
575+ }
576+
577+ /* A combined 'Match User X Group Y' directive is a conjunction: it applies only
578+ * to a user who satisfies BOTH selectors, matching OpenSSH semantics. This
579+ * locks in that wolfSSHD_GetUserConf does not return such a block for a user
580+ * who satisfies only one selector (the policy-bypass case), while single
581+ * selector 'Match User' and 'Match Group' blocks keep applying on their one
582+ * selector alone. */
583+ static int test_GetUserConfMatchGroupAnd (void )
584+ {
585+ int ret = WS_SUCCESS ;
586+ WOLFSSHD_CONFIG * head ;
587+ WOLFSSHD_CONFIG * conf ;
588+ WOLFSSHD_CONFIG * combined ;
589+ WOLFSSHD_CONFIG * userOnly ;
590+ WOLFSSHD_CONFIG * groupOnly ;
591+ WOLFSSHD_CONFIG * match ;
592+
593+ head = wolfSSHD_ConfigNew (NULL );
594+ if (head == NULL )
595+ ret = WS_MEMORY_E ;
596+ conf = head ;
574597
598+ #define PCL (s ) ParseConfigLine(&conf, s, (int)WSTRLEN(s), 0)
599+ /* restrictive global default: SFTP only for everyone */
600+ if (ret == WS_SUCCESS ) ret = PCL ("ForceCommand internal-sftp" );
601+
602+ /* combined block relaxes the restriction, but only for a user who is both
603+ * 'alice' AND in group 'admins' */
604+ if (ret == WS_SUCCESS ) ret = PCL ("Match User alice Group admins" );
605+ if (ret == WS_SUCCESS ) ret = PCL ("ForceCommand /bin/sh" );
606+ if (ret == WS_SUCCESS ) combined = conf ;
607+
608+ /* single selector blocks must keep matching on their one selector */
609+ if (ret == WS_SUCCESS ) ret = PCL ("Match User bob" );
610+ if (ret == WS_SUCCESS ) ret = PCL ("ForceCommand /bin/bob" );
611+ if (ret == WS_SUCCESS ) userOnly = conf ;
612+
613+ if (ret == WS_SUCCESS ) ret = PCL ("Match Group staff" );
614+ if (ret == WS_SUCCESS ) ret = PCL ("ForceCommand /bin/staff" );
615+ if (ret == WS_SUCCESS ) groupOnly = conf ;
616+ #undef PCL
617+
618+ /* alice in admins satisfies both selectors -> gets the combined block */
619+ if (ret == WS_SUCCESS ) {
620+ match = wolfSSHD_GetUserConf (head , "alice" , "admins" , NULL , NULL ,
621+ NULL , NULL , NULL );
622+ if (match != combined )
623+ ret = WS_FATAL_ERROR ;
624+ }
625+
626+ /* alice in a different group satisfies only the user selector -> must NOT
627+ * get the combined block; falls back to the restrictive global head */
628+ if (ret == WS_SUCCESS ) {
629+ match = wolfSSHD_GetUserConf (head , "alice" , "users" , NULL , NULL ,
630+ NULL , NULL , NULL );
631+ if (match != head )
632+ ret = WS_FATAL_ERROR ;
633+ }
634+
635+ /* carol in admins satisfies only the group selector -> must NOT get the
636+ * combined block; falls back to the restrictive global head */
637+ if (ret == WS_SUCCESS ) {
638+ match = wolfSSHD_GetUserConf (head , "carol" , "admins" , NULL , NULL ,
639+ NULL , NULL , NULL );
640+ if (match != head )
641+ ret = WS_FATAL_ERROR ;
642+ }
643+
644+ /* alice with no resolved group must NOT get the combined block: a NULL
645+ * group cannot satisfy the group selector, so it fails closed to the
646+ * global head. This is the WIN32 / failed group-lookup path where
647+ * wolfSSHD_AuthGetUserConf passes grp == NULL. */
648+ if (ret == WS_SUCCESS ) {
649+ match = wolfSSHD_GetUserConf (head , "alice" , NULL , NULL , NULL ,
650+ NULL , NULL , NULL );
651+ if (match != head )
652+ ret = WS_FATAL_ERROR ;
653+ }
654+
655+ /* single selector 'Match User bob' still applies on the user alone */
656+ if (ret == WS_SUCCESS ) {
657+ match = wolfSSHD_GetUserConf (head , "bob" , "anygroup" , NULL , NULL ,
658+ NULL , NULL , NULL );
659+ if (match != userOnly )
660+ ret = WS_FATAL_ERROR ;
661+ }
662+
663+ /* single selector 'Match Group staff' still applies on the group alone */
664+ if (ret == WS_SUCCESS ) {
665+ match = wolfSSHD_GetUserConf (head , "anyuser" , "staff" , NULL , NULL ,
666+ NULL , NULL , NULL );
667+ if (match != groupOnly )
668+ ret = WS_FATAL_ERROR ;
669+ }
670+
671+ wolfSSHD_ConfigFree (head );
575672 return ret ;
576673}
577674
@@ -1011,6 +1108,7 @@ const TEST_CASE testCases[] = {
10111108 TEST_DECL (test_ConfigCopy ),
10121109 TEST_DECL (test_GetUserConfMatchOverride ),
10131110 TEST_DECL (test_MatchUnsupportedSelector ),
1111+ TEST_DECL (test_GetUserConfMatchGroupAnd ),
10141112 TEST_DECL (test_CAKeysFileDiffers ),
10151113 TEST_DECL (test_IncludeRecursionBound ),
10161114 TEST_DECL (test_ConfigFree ),
0 commit comments