完善本地邮件服务与中继配置边界
This commit is contained in:
parent
5dbd23e1a6
commit
2ebab39973
@ -25,6 +25,7 @@ func DefaultMigrations() []Migration {
|
||||
DefineForDialects(13, "create_protocol_credentials", protocolCredentialStatements()),
|
||||
DefineCustom(14, "expand_imap_deleted_flag", "expand_imap_deleted_flag_v1", applyIMAPDeletedFlagSchema),
|
||||
DefineCustom(15, "expand_security_setup", "expand_security_setup_v1", applySecuritySetupSchema),
|
||||
DefineCustom(16, "expand_user_security_preferences", "expand_user_security_preferences_v1", applyUserSecurityPreferencesSchema),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ func TestDefaultMigrationsCreateCompleteSQLiteSchema(t *testing.T) {
|
||||
t.Fatalf("table %s was not created", table)
|
||||
}
|
||||
}
|
||||
for _, column := range []string{"role", "session_version", "security_version", "must_change_password", "password_changed_at", "security_setup_acknowledged"} {
|
||||
for _, column := range []string{"role", "session_version", "security_version", "must_change_password", "password_changed_at", "security_setup_acknowledged", "external_email_enabled", "webauthn_enabled"} {
|
||||
exists, err := columnExists(context.Background(), db, DialectSQLite, "user", column)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -85,11 +85,13 @@ func TestDefaultMigrationsUpgradeLegacyUserSchema(t *testing.T) {
|
||||
var securityVersion int
|
||||
var mustChangePassword int
|
||||
var passwordChangedAt any
|
||||
if err := db.QueryRow(`SELECT "role", "session_version", "security_version", "must_change_password", "password_changed_at" FROM "user" WHERE "account" = 'root'`).Scan(&role, &sessionVersion, &securityVersion, &mustChangePassword, &passwordChangedAt); err != nil {
|
||||
var externalEmailEnabled int
|
||||
var webAuthnEnabled int
|
||||
if err := db.QueryRow(`SELECT "role", "session_version", "security_version", "must_change_password", "password_changed_at", "external_email_enabled", "webauthn_enabled" FROM "user" WHERE "account" = 'root'`).Scan(&role, &sessionVersion, &securityVersion, &mustChangePassword, &passwordChangedAt, &externalEmailEnabled, &webAuthnEnabled); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if role != "super_admin" || sessionVersion != 1 || securityVersion != 1 || mustChangePassword != 0 || passwordChangedAt != nil {
|
||||
t.Fatalf("role = %q, session version = %d, security version = %d, must change password = %d, password changed at = %v", role, sessionVersion, securityVersion, mustChangePassword, passwordChangedAt)
|
||||
if role != "super_admin" || sessionVersion != 1 || securityVersion != 1 || mustChangePassword != 0 || passwordChangedAt != nil || externalEmailEnabled != 1 || webAuthnEnabled != 1 {
|
||||
t.Fatalf("role = %q, session version = %d, security version = %d, must change password = %d, password changed at = %v, external email enabled = %d, webauthn enabled = %d", role, sessionVersion, securityVersion, mustChangePassword, passwordChangedAt, externalEmailEnabled, webAuthnEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,6 +219,7 @@ func TestDefaultMigrationChecksumsAreImmutable(t *testing.T) {
|
||||
13: "c51604f27347d0a1b93acad1e33af53463fceb7b6573960f881f95f29c5628ed",
|
||||
14: "531398c2b8fc83a65753fae08666946bb253ac3da6596829e71cc8ccc4ddf62f",
|
||||
15: "59f0ff9acfed3619c17f078d21a1362ddd5f1feacb9a2d77626f69040d25de65",
|
||||
16: "",
|
||||
}
|
||||
migrations := DefaultMigrations()
|
||||
if len(migrations) != len(expected) {
|
||||
|
||||
@ -73,6 +73,30 @@ func applyUserLifecycleSchema(ctx context.Context, exec executor, dialect Dialec
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyUserSecurityPreferencesSchema(ctx context.Context, exec executor, dialect Dialect) error {
|
||||
columns := []struct {
|
||||
name string
|
||||
definition map[Dialect]string
|
||||
}{
|
||||
{name: "external_email_enabled", definition: map[Dialect]string{
|
||||
DialectSQLite: `INTEGER NOT NULL DEFAULT 1`,
|
||||
DialectMySQL: `INT UNSIGNED NOT NULL DEFAULT 1`,
|
||||
DialectPostgres: `INTEGER NOT NULL DEFAULT 1`,
|
||||
}},
|
||||
{name: "webauthn_enabled", definition: map[Dialect]string{
|
||||
DialectSQLite: `INTEGER NOT NULL DEFAULT 1`,
|
||||
DialectMySQL: `INT UNSIGNED NOT NULL DEFAULT 1`,
|
||||
DialectPostgres: `INTEGER NOT NULL DEFAULT 1`,
|
||||
}},
|
||||
}
|
||||
for _, column := range columns {
|
||||
if err := ensureColumn(ctx, exec, dialect, "user", column.name, column.definition[dialect]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verificationOperationsStatements() map[Dialect][]string {
|
||||
return map[Dialect][]string{
|
||||
DialectSQLite: {
|
||||
|
||||
@ -19,6 +19,8 @@ type User struct {
|
||||
MustChangePassword int `xorm:"must_change_password unsigned int not null default(0)" json:"must_change_password"`
|
||||
PasswordChangedAt *time.Time `xorm:"password_changed_at datetime null" json:"-"`
|
||||
SecuritySetupAcknowledged int `xorm:"security_setup_acknowledged not null default(0)" json:"-"`
|
||||
ExternalEmailEnabled int `xorm:"external_email_enabled unsigned int not null default(1)" json:"-"`
|
||||
WebAuthnEnabled int `xorm:"webauthn_enabled unsigned int not null default(1)" json:"-"`
|
||||
}
|
||||
|
||||
// EffectiveRole 返回新角色字段,兼容旧版本只保存 IsAdmin 的账户。
|
||||
|
||||
@ -297,43 +297,26 @@ func startVerificationRuntime() error {
|
||||
runtime.Stop()
|
||||
return fmt.Errorf("start external email repository: %w", err)
|
||||
}
|
||||
smtpSettings, err := config.CurrentSMTPRuntimeSettings()
|
||||
if err != nil {
|
||||
runtime.Stop()
|
||||
return err
|
||||
}
|
||||
if !smtpSettings.VerificationEnabled && !smtpSettings.ExternalRecoveryEnabled {
|
||||
recover, recoverErr := recovery.NewService(db.Instance, nil, external, credential)
|
||||
if recoverErr != nil {
|
||||
runtime.Stop()
|
||||
return fmt.Errorf("start recovery service: %w", recoverErr)
|
||||
delivery := verification.NewLazySMTPDelivery(func() (verification.SMTPSettings, error) {
|
||||
smtpSettings, err := config.CurrentSMTPRuntimeSettings()
|
||||
if err != nil {
|
||||
return verification.SMTPSettings{}, err
|
||||
}
|
||||
recover.SetWebAuthnVerifier(webAuthnRuntimeService)
|
||||
controllers.ConfigureSecurityRuntime(nil, external, recover, credential)
|
||||
credentialService = credential
|
||||
verificationRuntime = runtime
|
||||
log.Info("邮箱验证码服务已禁用")
|
||||
return nil
|
||||
}
|
||||
security, err := smtpSecurityMode(smtpSettings.Relay.Security)
|
||||
if err != nil {
|
||||
runtime.Stop()
|
||||
return err
|
||||
}
|
||||
delivery, err := verification.NewSMTPDelivery(verification.SMTPSettings{
|
||||
Host: smtpSettings.Relay.RelayHost,
|
||||
Port: smtpSettings.Relay.RelayPort,
|
||||
Username: smtpSettings.Relay.Username,
|
||||
Password: smtpSettings.Password,
|
||||
Security: security,
|
||||
From: smtpSettings.Relay.From,
|
||||
FromDomain: smtpSettings.Relay.FromDomain,
|
||||
security, err := smtpSecurityMode(smtpSettings.Relay.Security)
|
||||
if err != nil {
|
||||
return verification.SMTPSettings{}, err
|
||||
}
|
||||
return verification.SMTPSettings{
|
||||
Host: smtpSettings.Relay.RelayHost,
|
||||
Port: smtpSettings.Relay.RelayPort,
|
||||
Username: smtpSettings.Relay.Username,
|
||||
Password: smtpSettings.Password,
|
||||
Security: security,
|
||||
From: smtpSettings.Relay.From,
|
||||
FromDomain: smtpSettings.Relay.FromDomain,
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
runtime.Stop()
|
||||
return fmt.Errorf("SMTP 投递服务初始化失败: %w", err)
|
||||
}
|
||||
service, err := verification.NewService(runtime.Store, delivery.Delivery(), runtime.Events)
|
||||
service, err := verification.NewService(runtime.Store, delivery, runtime.Events)
|
||||
if err != nil {
|
||||
runtime.Stop()
|
||||
return fmt.Errorf("start verification service: %w", err)
|
||||
@ -348,7 +331,7 @@ func startVerificationRuntime() error {
|
||||
controllers.ConfigureSecurityRuntime(service, external, recover, credential)
|
||||
credentialService = credential
|
||||
verificationRuntime = runtime
|
||||
log.Info("邮箱验证码服务已启动")
|
||||
log.Info("安全验证码服务已启动,外发邮件将在实际使用时检查 SMTP 中继配置")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -374,9 +357,6 @@ func startWebAuthnRuntime() error {
|
||||
}
|
||||
snapshot := manager.Snapshot()
|
||||
security := snapshot.Root.Security
|
||||
if !security.WebAuthnEnabled {
|
||||
return nil
|
||||
}
|
||||
service, err := webauthnservice.NewService(db.Instance, webauthnservice.Config{
|
||||
RPID: security.WebAuthnRPID,
|
||||
RPDisplayName: security.WebAuthnRPDisplayName,
|
||||
|
||||
@ -24,6 +24,7 @@ const (
|
||||
var (
|
||||
ErrInvalidSMTPConfig = errors.New("invalid SMTP configuration")
|
||||
ErrUnsupportedSMTP = errors.New("unsupported SMTP security mode")
|
||||
ErrSMTPRelayUnavailable = errors.New("SMTP relay is unavailable")
|
||||
)
|
||||
|
||||
type SMTPSettings struct {
|
||||
@ -59,6 +60,24 @@ func (d *SMTPDelivery) Delivery() Delivery {
|
||||
}
|
||||
}
|
||||
|
||||
// NewLazySMTPDelivery 延迟读取 SMTP 中继配置,避免中继未配置时阻塞本地邮件服务启动。
|
||||
func NewLazySMTPDelivery(load func() (SMTPSettings, error)) Delivery {
|
||||
return func(challenge Challenge, code string) error {
|
||||
if load == nil || challenge.Email == "" || code == "" {
|
||||
return ErrSMTPRelayUnavailable
|
||||
}
|
||||
settings, err := load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load SMTP relay settings: %w", err)
|
||||
}
|
||||
delivery, err := NewSMTPDelivery(settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP relay is unavailable: %w", err)
|
||||
}
|
||||
return delivery.Delivery()(challenge, code)
|
||||
}
|
||||
}
|
||||
|
||||
func IssueWithSMTP(store *Store, request IssueRequest, settings SMTPSettings) (Challenge, error) {
|
||||
if store == nil {
|
||||
return Challenge{}, ErrInvalidChallenge
|
||||
|
||||
Loading…
Reference in New Issue
Block a user