Ctrl K
ComunidadGuías…

internal/server/two_factor_policy_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: Apache-2.0

package server

import (
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"testing"

	appdb "github.com/X4Applegate/caddyui/internal/db"
	"github.com/X4Applegate/caddyui/internal/models"
)

// v2.64.1 (issue #138): CaddyUI shipped two settings for one policy —
// "Require 2FA for all users" and "Require TOTP for all users" — with nearly
// identical descriptions. They are now one toggle. The risk in collapsing
// them is an install that had only one of the pair switched on quietly
// losing two-factor enforcement on upgrade, so that is what these pin down.

func twoFactorTestServer(t *testing.T) *Server {
	t.Helper()
	conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = conn.Close() })
	return &Server{DB: conn}
}

func TestEitherStoredKeyStillRequiresTwoFactor(t *testing.T) {
	for name, stored := range map[string]map[string]string{
		"only the deprecated require_2fa (pre-upgrade install)": {settingRequire2FA: "1", settingRequireTOTP: "0"},
		"only the canonical require_totp":                       {settingRequire2FA: "0", settingRequireTOTP: "1"},
		"both, as saving now writes them":                       {settingRequire2FA: "1", settingRequireTOTP: "1"},
	} {
		t.Run(name, func(t *testing.T) {
			s := twoFactorTestServer(t)
			for k, v := range stored {
				if err := models.SetSetting(s.DB, k, v); err != nil {
					t.Fatal(err)
				}
			}
			if !s.twoFactorRequired() {
				t.Error("two-factor enforcement was dropped; an admin who had it on would silently lose it")
			}
		})
	}
}

func TestTwoFactorIsOffWhenNeitherKeyIsSet(t *testing.T) {
	s := twoFactorTestServer(t)
	for _, k := range []string{settingRequire2FA, settingRequireTOTP} {
		if err := models.SetSetting(s.DB, k, "0"); err != nil {
			t.Fatal(err)
		}
	}
	if s.twoFactorRequired() {
		t.Error("nothing is set, so the policy must be off")
	}
}

// Saving must write both keys, so downgrading to a release that reads only
// the deprecated key still enforces the policy.
func TestSavingThePolicyWritesBothKeys(t *testing.T) {
	for _, tc := range []struct {
		name, submitted, want string
	}{
		{"checked, value=1 convention", "1", "1"},
		{"checked, bare on convention", "on", "1"},
		{"unchecked", "", "0"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			form := url.Values{}
			if tc.submitted != "" {
				form.Set("require_totp", tc.submitted)
			}
			r := httptest.NewRequest("POST", "/settings/security", strings.NewReader(form.Encode()))
			r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
			if got := requireTOTPFormValue(r); got != tc.want {
				t.Errorf("requireTOTPFormValue = %q, want %q", got, tc.want)
			}
		})
	}
}

// The old require_2fa block let /api/ through and the require_totp block did
// not, so whether a JSON caller got a redirect instead of a response depended
// on which of the two identical-looking toggles the admin had used. The
// surviving check keeps the wider exclusion list.
func TestEnforcementExemptPathsAreTheWiderSet(t *testing.T) {
	// Mirrors the condition in requireAuth; kept as a table so a change to
	// the exclusions has to be deliberate.
	exempt := func(path string) bool {
		return strings.HasPrefix(path, "/totp/") || strings.HasPrefix(path, "/static/") ||
			strings.HasPrefix(path, "/api/") || path == "/logout"
	}
	for path, want := range map[string]bool{
		"/totp/setup":                   true, // enrolment page
		"/totp/regenerate-backup-codes": true, // and its siblings
		"/static/app.css":               true,
		"/api/v1/proxy-hosts":           true, // a redirect here would break JSON callers
		"/logout":                       true,
		"/":                             false,
		"/proxy-hosts":                  false,
		"/settings/security":            false,
	} {
		if got := exempt(path); got != want {
			t.Errorf("exempt(%q) = %v, want %v", path, got, want)
		}
	}
}