fix: Reset 2FA keeps the privileged-group guard

Resetting the second factors of a privileged-group member now needs
golem15.users.manage_privileged_groups, as changing their password or
deleting them does; without it the action answers 403 before Reset.
This commit is contained in:
2026-10-26 16:04:11 +01:00
parent cc3636cea9
commit 20e7b175f8
5 changed files with 63 additions and 7 deletions

View File

@@ -11,6 +11,7 @@ import (
"git.golem15.com/golem15/sm-user-plugin/classes"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/pact"
"gorm.io/gorm"
)
@@ -70,7 +71,10 @@ func resetAction(t *testing.T, app *backpack.App) pact.AdminRecordAction {
// offered with the danger confirmation when one is published and reports a
// second factor; hidden when it reports none; refused without
// golem15.users.reset_two_factor; Run needs the action's transaction, runs
// Reset in it, and Reset's error rolls the transaction back.
// Reset in it, and Reset's error rolls the transaction back. A member of a
// privileged group keeps the takeover guard: without
// golem15.users.manage_privileged_groups Run answers 403 with the guard's
// message before Reset.
func TestResetTwoFactorAction(t *testing.T) {
if testing.Short() {
t.Skip("Postgres")
@@ -172,6 +176,50 @@ func TestResetTwoFactorAction(t *testing.T) {
}
})
t.Run("privileged_member_needs_the_privileged_permission", func(t *testing.T) {
code := "rta-priv-" + adminStamp()
env := newAdminEnv(t, func(cfg *compass.Config) {
if err := cfg.Set("golem15.user.privileged_groups", []string{"admin", code}); err != nil {
t.Fatal(err)
}
})
svc := &scriptedTwoFactorAdmin{has: true}
if err := env.app.Publish[classes.TwoFactorAdmin](svc); err != nil {
t.Fatal(err)
}
limited := env.admin(t, permAccessUsers, permResetTwoFactor)
manager := env.admin(t, permAccessUsers, permResetTwoFactor, permManagePrivileged)
member := env.seedUser(t, "rta-privileged", nil)
env.join(t, member, env.seedGroup(t, "Privileged "+code, code))
ordinary := env.seedUser(t, "rta-ordinary", nil)
rec := env.expect(t, http.StatusForbidden, http.MethodPost, userPath(member.ID, "/actions/reset_two_factor"), limited, map[string]any{})
answer := adminDecode(t, rec)
const want = "This user belongs to a privileged group. Resetting their two-factor authentication needs the permission to manage privileged groups. Nothing was changed."
if answer.Error.Code != "forbidden" || answer.Error.Message != want {
t.Fatalf("refusal = %s", rec.Body.String())
}
if len(svc.calls()) != 0 {
t.Fatalf("a refused reset reached Reset: %v", svc.calls())
}
if name := env.reload(t, member.ID).Name; name == nil || *name != "rta-privileged" {
t.Fatalf("the refused reset changed the member: name = %v", name)
}
// An ordinary user stays resettable with the reset permission alone.
if !slices.Contains(env.offered(t, limited, ordinary.ID), "reset_two_factor") {
t.Fatal("not offered for an ordinary user")
}
env.expect(t, http.StatusOK, http.MethodPost, userPath(ordinary.ID, "/actions/reset_two_factor"), limited, map[string]any{})
// With the privileged permission the member is offered and reset.
if !slices.Contains(env.offered(t, manager, member.ID), "reset_two_factor") {
t.Fatal("not offered to an administrator who manages privileged groups")
}
env.expect(t, http.StatusOK, http.MethodPost, userPath(member.ID, "/actions/reset_two_factor"), manager, map[string]any{})
if calls := svc.calls(); len(calls) != 2 || calls[0] != ordinary.ID || calls[1] != member.ID {
t.Fatalf("Reset calls = %v", calls)
}
})
t.Run("run_error_rolls_back", func(t *testing.T) {
env := newAdminEnv(t)
svc := &scriptedTwoFactorAdmin{has: true, resetErr: errors.New("credential delete failed")}