Skip to content

Commit 3af8cef

Browse files
Fix stale session permissions after wh_Auth_UserSetPermissions
1 parent a288dc4 commit 3af8cef

4 files changed

Lines changed: 190 additions & 2 deletions

File tree

docs/src/5-Features.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,17 @@ Credential updates through `wh_Auth_UserSetCredentials` add a further check on t
12011201

12021202
`wh_Auth_UserDelete` and `wh_Auth_UserSetPermissions` remain admin-only operations in the base backend.
12031203

1204+
Independently of the backend, the core refuses any `wh_Auth_UserSetPermissions` call from a non-admin session whose supplied permissions **carry** the admin flag. The check is absolute rather than a promotion check: the core cannot see whether the target already held the flag, so it refuses even a request that would merely preserve an existing admin. That is the only permission policy the core enforces when changing an existing user. Subset limits on the remaining group, action, and key-id bits — and admin **revocation**, including demotion of the last remaining admin — are delegated to the backend. The core is handed only the target's `whUserId` and has no way to read that user's current permissions (`UserGet` is keyed by username), so it can enforce absolute rules but not rules relative to the target's existing state. A backend that allows non-admin permission edits is responsible for its own escalation and lockout policy.
1205+
1206+
### Permission Changes and Live Sessions
1207+
1208+
A permission change made through `wh_Auth_UserSetPermissions` takes effect on the calling session immediately: when the target `user_id` is the user logged in on that context, the cached session permissions are refreshed with the supplied values, so a revoked group or action bit is enforced on the very next request rather than at the next login. The cache mirrors what the caller supplied, so a backend that stores something other than the permissions it was handed leaves the session cache diverged from its own record.
1209+
1210+
Two consequences are worth planning for:
1211+
1212+
- **Self-demotion is immediate and can be self-locking.** A session that clears its own group or action bits loses those capabilities at once. If it clears the `USER_SET_PERMISSIONS` action, it can no longer restore itself and must log out and back in as a sufficiently privileged user.
1213+
- **Other sessions are not affected.** Permissions are cached per `whAuthContext`, and there is no cross-context notification. A user logged in on another connection keeps its existing permissions until it logs in again.
1214+
12041215
### Pluggable Backend
12051216

12061217
The authentication manager does not own the user database itself. All operations that read or modify user state — login, user add/delete, permission updates, credential updates — are dispatched through a `whAuthCb` callback table that the application supplies at server initialization. The storage backend is therefore a port-time decision: an in-memory table for development, an NVM-backed store for production, or a connector to an external identity service.

src/wh_auth.c

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,24 @@ int wh_Auth_UserSetPermissions(whAuthContext* context, whUserId user_id,
472472
return rc;
473473
}
474474

475-
rc = context->cb->UserSetPermissions(
476-
context->context, context->user.user_id, user_id, permissions);
475+
/* A non-admin session can not set the admin flag, no matter what the
476+
* backend policy allows. Absolute check; the target's current flag is
477+
* not visible here. */
478+
if (!WH_AUTH_IS_ADMIN(context->user.permissions) &&
479+
WH_AUTH_IS_ADMIN(permissions)) {
480+
rc = WH_AUTH_PERMISSION_ERROR;
481+
}
482+
else {
483+
rc = context->cb->UserSetPermissions(
484+
context->context, context->user.user_id, user_id, permissions);
485+
486+
/* Keep the live session cache in sync when a logged-in user changes
487+
* its own permissions; authorization reads only this cache. */
488+
if ((rc == WH_ERROR_OK) && (user_id == context->user.user_id) &&
489+
(context->user.user_id != WH_USER_ID_INVALID)) {
490+
context->user.permissions = permissions;
491+
}
492+
}
477493

478494
(void)WH_AUTH_UNLOCK(context);
479495
return rc;

test-refactor/client-server/wh_test_auth.c

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,8 +568,20 @@ static int _whTest_AuthSetPermissions_impl(whClientContext* client)
568568
whUserId user_id;
569569
whAuthPermissions perms, new_perms;
570570
whAuthPermissions fetched_perms;
571+
whAuthPermissions full_perms, demoted_perms;
572+
whAuthPermissions nonadmin_perms, escalate_perms;
571573
whUserId fetched_user_id = WH_USER_ID_INVALID;
574+
whUserId probe_id = WH_USER_ID_INVALID;
575+
whUserId nonadmin_id = WH_USER_ID_INVALID;
572576
int32_t get_rc = 0;
577+
int32_t probe_server_rc = 0;
578+
int32_t plain_set_rc = 0;
579+
int32_t self_grant_rc = 0;
580+
int32_t other_grant_rc = 0;
581+
int32_t target_get_rc = 0;
582+
int probe_rc = 0;
583+
int nonadmin_admin = 0;
584+
int target_admin = 0;
573585

574586
/* Login as admin first */
575587
whAuthPermissions admin_perms;
@@ -643,6 +655,151 @@ static int _whTest_AuthSetPermissions_impl(whClientContext* client)
643655
WH_TEST_ASSERT_RETURN(permissions_match);
644656
}
645657

658+
/* Test 2c: self-demote must bind the live session. Admin clears its own
659+
* USER_ADD bit (keeping admin + SET_PERMISSIONS so it can restore). */
660+
WH_TEST_PRINT(" Test: Self-demote binds live session\n");
661+
memset(&full_perms, 0, sizeof(full_perms));
662+
fetched_user_id = WH_USER_ID_INVALID;
663+
get_rc = 0;
664+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserGetOp(client, TEST_ADMIN_USERNAME,
665+
&get_rc, &fetched_user_id,
666+
&full_perms));
667+
WH_TEST_ASSERT_RETURN(get_rc == WH_ERROR_OK);
668+
WH_TEST_ASSERT_RETURN(fetched_user_id == admin_id);
669+
670+
demoted_perms = full_perms;
671+
WH_AUTH_CLEAR_ALLOWED_ACTION(demoted_perms, WH_MESSAGE_GROUP_AUTH,
672+
WH_MESSAGE_AUTH_ACTION_USER_ADD);
673+
server_rc = 0;
674+
WH_TEST_RETURN_ON_FAIL(
675+
_whTest_Auth_UserSetPermsOp(client, admin_id, demoted_perms,
676+
&server_rc));
677+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
678+
679+
/* The revoked bit must now deny the admin's own UserAdd. Capture the
680+
* probe result, restore, and only then assert -- a failing assert here
681+
* must not leave the stored admin record demoted for later tests. */
682+
memset(&perms, 0, sizeof(perms));
683+
probe_server_rc = 0;
684+
probe_id = WH_USER_ID_INVALID;
685+
probe_rc = _whTest_Auth_UserAddOp(client, "selfdemote_probe", perms,
686+
WH_AUTH_METHOD_PIN, "pass", 4,
687+
&probe_server_rc, &probe_id);
688+
689+
/* Restore full perms before asserting the probe outcome. */
690+
server_rc = 0;
691+
WH_TEST_RETURN_ON_FAIL(
692+
_whTest_Auth_UserSetPermsOp(client, admin_id, full_perms, &server_rc));
693+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
694+
695+
/* If the probe wrongly succeeded it consumed a user slot, so drop it
696+
* before asserting to keep the 5-slot table clean for later tests. */
697+
_whTest_Auth_DeleteUserByName(client, "selfdemote_probe");
698+
699+
/* A denial is a well-formed response, so the client call itself is OK */
700+
WH_TEST_ASSERT_RETURN(probe_rc == WH_ERROR_OK);
701+
WH_TEST_ASSERT_RETURN(probe_server_rc != WH_ERROR_OK);
702+
WH_TEST_ASSERT_RETURN(probe_id == WH_USER_ID_INVALID);
703+
704+
/* The same session can add again after the restore. */
705+
memset(&perms, 0, sizeof(perms));
706+
server_rc = 0;
707+
probe_id = WH_USER_ID_INVALID;
708+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(client, "selfdemote_probe",
709+
perms, WH_AUTH_METHOD_PIN,
710+
"pass", 4, &server_rc,
711+
&probe_id));
712+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
713+
WH_TEST_ASSERT_RETURN(probe_id != WH_USER_ID_INVALID);
714+
_whTest_Auth_DeleteUserByName(client, "selfdemote_probe");
715+
716+
/* Changing a different user's permissions leaves the caller's own
717+
* session untouched: admin can still add after demoting testuser3. */
718+
memset(&new_perms, 0, sizeof(new_perms));
719+
server_rc = 0;
720+
WH_TEST_RETURN_ON_FAIL(
721+
_whTest_Auth_UserSetPermsOp(client, user_id, new_perms, &server_rc));
722+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
723+
724+
memset(&perms, 0, sizeof(perms));
725+
server_rc = 0;
726+
probe_id = WH_USER_ID_INVALID;
727+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(client, "selfdemote_probe2",
728+
perms, WH_AUTH_METHOD_PIN,
729+
"pass", 4, &server_rc,
730+
&probe_id));
731+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
732+
WH_TEST_ASSERT_RETURN(probe_id != WH_USER_ID_INVALID);
733+
_whTest_Auth_DeleteUserByName(client, "selfdemote_probe2");
734+
735+
/* Test 2d: a non-admin session can not hand out the admin flag. */
736+
WH_TEST_PRINT(" Test: Non-admin cannot grant admin permissions\n");
737+
memset(&nonadmin_perms, 0, sizeof(nonadmin_perms));
738+
WH_AUTH_SET_ALLOWED_ACTION(nonadmin_perms, WH_MESSAGE_GROUP_AUTH,
739+
WH_MESSAGE_AUTH_ACTION_USER_SET_PERMISSIONS);
740+
WH_AUTH_SET_IS_ADMIN(nonadmin_perms, 0);
741+
server_rc = 0;
742+
nonadmin_id = WH_USER_ID_INVALID;
743+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(
744+
client, "setperms_nonadmin", nonadmin_perms, WH_AUTH_METHOD_PIN, "pass",
745+
4, &server_rc, &nonadmin_id));
746+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
747+
WH_TEST_ASSERT_RETURN(nonadmin_id != WH_USER_ID_INVALID);
748+
749+
_whTest_Auth_LogoutOp(client, admin_id, &server_rc);
750+
server_rc = 0;
751+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN,
752+
"setperms_nonadmin", "pass", 4,
753+
&server_rc, &nonadmin_id));
754+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
755+
756+
/* Capture the outcomes first; asserting here would return with the
757+
* non-admin session still live. */
758+
escalate_perms = nonadmin_perms;
759+
WH_AUTH_SET_IS_ADMIN(escalate_perms, 1);
760+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserSetPermsOp(
761+
client, nonadmin_id, nonadmin_perms, &plain_set_rc));
762+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserSetPermsOp(
763+
client, nonadmin_id, escalate_perms, &self_grant_rc));
764+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserSetPermsOp(
765+
client, user_id, escalate_perms, &other_grant_rc));
766+
767+
_whTest_Auth_LogoutOp(client, nonadmin_id, &server_rc);
768+
server_rc = 0;
769+
WH_TEST_RETURN_ON_FAIL(
770+
_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME,
771+
TEST_ADMIN_PIN, 4, &server_rc, &admin_id));
772+
WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK);
773+
774+
/* The backend refuses the plain call; the admin flag is refused earlier,
775+
* in the core, whatever the target. */
776+
WH_TEST_ASSERT_RETURN(plain_set_rc == WH_ERROR_ACCESS);
777+
WH_TEST_ASSERT_RETURN(self_grant_rc == WH_AUTH_PERMISSION_ERROR);
778+
WH_TEST_ASSERT_RETURN(other_grant_rc == WH_AUTH_PERMISSION_ERROR);
779+
780+
/* Neither stored record may have picked up the admin flag. */
781+
memset(&fetched_perms, 0, sizeof(fetched_perms));
782+
fetched_user_id = WH_USER_ID_INVALID;
783+
get_rc = 0;
784+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserGetOp(client, "setperms_nonadmin",
785+
&get_rc, &fetched_user_id,
786+
&fetched_perms));
787+
nonadmin_admin = WH_AUTH_IS_ADMIN(fetched_perms) ? 1 : 0;
788+
789+
memset(&fetched_perms, 0, sizeof(fetched_perms));
790+
fetched_user_id = WH_USER_ID_INVALID;
791+
target_get_rc = 0;
792+
WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserGetOp(
793+
client, "testuser3", &target_get_rc, &fetched_user_id, &fetched_perms));
794+
target_admin = WH_AUTH_IS_ADMIN(fetched_perms) ? 1 : 0;
795+
796+
_whTest_Auth_DeleteUserByName(client, "setperms_nonadmin");
797+
798+
WH_TEST_ASSERT_RETURN(get_rc == WH_ERROR_OK);
799+
WH_TEST_ASSERT_RETURN(target_get_rc == WH_ERROR_OK);
800+
WH_TEST_ASSERT_RETURN(!nonadmin_admin);
801+
WH_TEST_ASSERT_RETURN(!target_admin);
802+
646803
/* Test 3: Set user permissions for non-existent user */
647804
WH_TEST_PRINT(" Test: Set user permissions for non-existent user\n");
648805
memset(&new_perms, 0, sizeof(new_perms));

wolfhsm/wh_auth.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@ int wh_Auth_UserDelete(whAuthContext* context, whUserId user_id);
338338
/**
339339
* @brief Set user permissions.
340340
*
341+
* On success, a change targeting this context's logged-in user also refreshes
342+
* its cached session permissions, so it binds immediately. A non-admin session
343+
* supplying the admin flag is refused with WH_AUTH_PERMISSION_ERROR.
344+
*
341345
* @param[in] context Pointer to the auth context.
342346
* @param[in] user_id The user ID to set permissions for.
343347
* @param[in] permissions The new permissions to set.

0 commit comments

Comments
 (0)