212 lines
6.1 KiB
Go
212 lines
6.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Smail/config"
|
|
"git.pcux.cn/xuans/Smail/db"
|
|
"git.pcux.cn/xuans/Smail/models"
|
|
"git.pcux.cn/xuans/Smail/services/credentials"
|
|
"git.pcux.cn/xuans/Smail/utils/context"
|
|
"git.pcux.cn/xuans/Smail/utils/password"
|
|
)
|
|
|
|
func TestAuthenticateUsesArgon2idOnly(t *testing.T) {
|
|
config.Instance = &config.Config{
|
|
DbType: config.DBTypeSQLite,
|
|
DbDSN: filepath.Join(t.TempDir(), "auth.db"),
|
|
}
|
|
if err := db.Init(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = db.Instance.Close()
|
|
})
|
|
|
|
user := models.User{Account: "test-user", Name: "测试用户", IsAdmin: 1}
|
|
if err := SetPassword(&user, "CorrectP12"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(user.Password, "$argon2id$") || !password.Verify(user.Password, "CorrectP12") {
|
|
t.Fatal("测试密码哈希格式或校验失败")
|
|
}
|
|
if _, err := db.Instance.Insert(&user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var loaded models.User
|
|
if _, err := db.Instance.Where("account = ?", "test-user").Get(&loaded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if loaded.ID == 0 || !strings.HasPrefix(loaded.Password, "$argon2id$") {
|
|
t.Fatalf("测试用户未正确写入")
|
|
}
|
|
if _, err := Authenticate("test-user", "CorrectP12"); err != nil {
|
|
t.Fatalf("新密码认证失败: %v", err)
|
|
}
|
|
if _, err := Authenticate("test-user", "WrongPass12"); err == nil {
|
|
t.Fatal("错误密码不应认证成功")
|
|
}
|
|
if _, err := Authenticate("test-user", ""); err == nil {
|
|
t.Fatal("空密码不应认证成功")
|
|
}
|
|
}
|
|
|
|
func TestAuthenticateRejectsLegacyHash(t *testing.T) {
|
|
config.Instance = &config.Config{
|
|
DbType: config.DBTypeSQLite,
|
|
DbDSN: filepath.Join(t.TempDir(), "legacy.db"),
|
|
}
|
|
if err := db.Init(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = db.Instance.Close()
|
|
})
|
|
|
|
user := models.User{Account: "legacy-user", Name: "旧算法用户", Password: "4f3b2f8f3f8f3f8f3f8f3f8f3f8f3f8f"}
|
|
if _, err := db.Instance.Insert(&user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Authenticate("legacy-user", "legacy-password"); err == nil {
|
|
t.Fatal("旧算法哈希不应认证成功")
|
|
}
|
|
}
|
|
|
|
func TestHasAuthRequiresMailboxOwnershipForAdministrators(t *testing.T) {
|
|
oldConfig := config.Instance
|
|
oldDB := db.Instance
|
|
config.Instance = &config.Config{
|
|
DbType: config.DBTypeSQLite,
|
|
DbDSN: filepath.Join(t.TempDir(), "mailbox-auth.db"),
|
|
}
|
|
if err := db.Init(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if db.Instance != nil {
|
|
_ = db.Instance.Close()
|
|
}
|
|
db.Instance = oldDB
|
|
config.Instance = oldConfig
|
|
})
|
|
|
|
email := models.Email{MsgID: "mailbox-auth@example.com"}
|
|
if _, err := db.Instance.Insert(&email); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Instance.Insert(&models.UserEmail{UserID: 1, EmailID: email.Id}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !HasAuth(&context.Context{UserID: 1}, &email) {
|
|
t.Fatal("mailbox owner was rejected")
|
|
}
|
|
if HasAuth(&context.Context{UserID: 2, IsAdmin: true}, &email) {
|
|
t.Fatal("administrator bypassed mailbox ownership")
|
|
}
|
|
}
|
|
|
|
func TestProtocolAuthenticationAcceptsConfiguredFullEmailIdentifier(t *testing.T) {
|
|
oldConfig := config.Instance
|
|
oldDB := db.Instance
|
|
config.Instance = &config.Config{
|
|
DbType: config.DBTypeSQLite,
|
|
DbDSN: filepath.Join(t.TempDir(), "full-email-auth.db"),
|
|
Domain: "example.com",
|
|
Domains: []string{"example.com"},
|
|
}
|
|
if err := db.Init(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if db.Instance != nil && db.Instance != oldDB {
|
|
_ = db.Instance.Close()
|
|
}
|
|
db.Instance = oldDB
|
|
config.Instance = oldConfig
|
|
})
|
|
|
|
user := models.User{Account: "alice", Name: "Alice"}
|
|
if err := SetPassword(&user, "CorrectP12"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Instance.Insert(&user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
verifiedAt := time.Now().UTC()
|
|
canonicalAddress := "alice@example.com"
|
|
if _, err := db.Instance.Insert(&models.UserEmail{
|
|
UserID: user.ID,
|
|
Address: "alice@example.com",
|
|
AddressCanonical: &canonicalAddress,
|
|
VerifiedAt: &verifiedAt,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
service, err := credentials.NewService(db.Instance)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, protocol := range []string{"smtp", "imap", "pop3"} {
|
|
if err := service.SetProtocolEnabled(user.ID, protocol, true); err != nil {
|
|
t.Fatalf("enable %s: %v", protocol, err)
|
|
}
|
|
code, err := service.GenerateProtocolCode(user.ID, protocol)
|
|
if err != nil {
|
|
t.Fatalf("generate %s code: %v", protocol, err)
|
|
}
|
|
got, err := AuthenticateProtocol(protocol, "192.0.2.10:12345", " Alice@Example.com ", code)
|
|
if err != nil {
|
|
t.Fatalf("%s full email authentication failed: %v", protocol, err)
|
|
}
|
|
if got == nil || got.Account != "alice" {
|
|
t.Fatalf("%s authenticated user = %+v", protocol, got)
|
|
}
|
|
if _, err := AuthenticateProtocol(protocol, "192.0.2.10:12345", "alice@example.com", "CorrectP12"); err == nil {
|
|
t.Fatalf("%s must reject the web password", protocol)
|
|
}
|
|
}
|
|
|
|
if _, err := AuthenticateProtocol("imap", "192.0.2.10:12345", "alice", "CorrectP12"); err == nil {
|
|
t.Fatal("account-only protocol login should be rejected")
|
|
}
|
|
if _, err := AuthenticateProtocol("imap", "192.0.2.10:12345", "alice@other.example", "CorrectP12"); err == nil {
|
|
t.Fatal("unconfigured email domain should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestAuthenticateDoesNotTruncateFullEmailIdentifier(t *testing.T) {
|
|
oldConfig := config.Instance
|
|
oldDB := db.Instance
|
|
config.Instance = &config.Config{
|
|
DbType: config.DBTypeSQLite,
|
|
DbDSN: filepath.Join(t.TempDir(), "no-truncation-auth.db"),
|
|
Domain: "example.com",
|
|
Domains: []string{"example.com"},
|
|
}
|
|
if err := db.Init(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if db.Instance != nil && db.Instance != oldDB {
|
|
_ = db.Instance.Close()
|
|
}
|
|
db.Instance = oldDB
|
|
config.Instance = oldConfig
|
|
})
|
|
|
|
user := models.User{Account: "alice", Name: "Alice"}
|
|
if err := SetPassword(&user, "CorrectP12"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Instance.Insert(&user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Authenticate("alice@example.com", "CorrectP12"); err == nil {
|
|
t.Fatal("direct account authentication must not truncate a full email identifier")
|
|
}
|
|
}
|