完善本地邮件服务与中继配置边界
This commit is contained in:
parent
5dbd23e1a6
commit
2ebab39973
@ -25,6 +25,7 @@ func DefaultMigrations() []Migration {
|
|||||||
DefineForDialects(13, "create_protocol_credentials", protocolCredentialStatements()),
|
DefineForDialects(13, "create_protocol_credentials", protocolCredentialStatements()),
|
||||||
DefineCustom(14, "expand_imap_deleted_flag", "expand_imap_deleted_flag_v1", applyIMAPDeletedFlagSchema),
|
DefineCustom(14, "expand_imap_deleted_flag", "expand_imap_deleted_flag_v1", applyIMAPDeletedFlagSchema),
|
||||||
DefineCustom(15, "expand_security_setup", "expand_security_setup_v1", applySecuritySetupSchema),
|
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)
|
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)
|
exists, err := columnExists(context.Background(), db, DialectSQLite, "user", column)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -85,11 +85,13 @@ func TestDefaultMigrationsUpgradeLegacyUserSchema(t *testing.T) {
|
|||||||
var securityVersion int
|
var securityVersion int
|
||||||
var mustChangePassword int
|
var mustChangePassword int
|
||||||
var passwordChangedAt any
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if role != "super_admin" || sessionVersion != 1 || securityVersion != 1 || mustChangePassword != 0 || passwordChangedAt != nil {
|
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", role, sessionVersion, securityVersion, mustChangePassword, passwordChangedAt)
|
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",
|
13: "c51604f27347d0a1b93acad1e33af53463fceb7b6573960f881f95f29c5628ed",
|
||||||
14: "531398c2b8fc83a65753fae08666946bb253ac3da6596829e71cc8ccc4ddf62f",
|
14: "531398c2b8fc83a65753fae08666946bb253ac3da6596829e71cc8ccc4ddf62f",
|
||||||
15: "59f0ff9acfed3619c17f078d21a1362ddd5f1feacb9a2d77626f69040d25de65",
|
15: "59f0ff9acfed3619c17f078d21a1362ddd5f1feacb9a2d77626f69040d25de65",
|
||||||
|
16: "",
|
||||||
}
|
}
|
||||||
migrations := DefaultMigrations()
|
migrations := DefaultMigrations()
|
||||||
if len(migrations) != len(expected) {
|
if len(migrations) != len(expected) {
|
||||||
|
|||||||
@ -73,6 +73,30 @@ func applyUserLifecycleSchema(ctx context.Context, exec executor, dialect Dialec
|
|||||||
return nil
|
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 {
|
func verificationOperationsStatements() map[Dialect][]string {
|
||||||
return map[Dialect][]string{
|
return map[Dialect][]string{
|
||||||
DialectSQLite: {
|
DialectSQLite: {
|
||||||
|
|||||||
@ -19,6 +19,8 @@ type User struct {
|
|||||||
MustChangePassword int `xorm:"must_change_password unsigned int not null default(0)" json:"must_change_password"`
|
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:"-"`
|
PasswordChangedAt *time.Time `xorm:"password_changed_at datetime null" json:"-"`
|
||||||
SecuritySetupAcknowledged int `xorm:"security_setup_acknowledged not null default(0)" 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 的账户。
|
// EffectiveRole 返回新角色字段,兼容旧版本只保存 IsAdmin 的账户。
|
||||||
|
|||||||
@ -297,43 +297,26 @@ func startVerificationRuntime() error {
|
|||||||
runtime.Stop()
|
runtime.Stop()
|
||||||
return fmt.Errorf("start external email repository: %w", err)
|
return fmt.Errorf("start external email repository: %w", err)
|
||||||
}
|
}
|
||||||
smtpSettings, err := config.CurrentSMTPRuntimeSettings()
|
delivery := verification.NewLazySMTPDelivery(func() (verification.SMTPSettings, error) {
|
||||||
if err != nil {
|
smtpSettings, err := config.CurrentSMTPRuntimeSettings()
|
||||||
runtime.Stop()
|
if err != nil {
|
||||||
return err
|
return verification.SMTPSettings{}, 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)
|
|
||||||
}
|
}
|
||||||
recover.SetWebAuthnVerifier(webAuthnRuntimeService)
|
security, err := smtpSecurityMode(smtpSettings.Relay.Security)
|
||||||
controllers.ConfigureSecurityRuntime(nil, external, recover, credential)
|
if err != nil {
|
||||||
credentialService = credential
|
return verification.SMTPSettings{}, err
|
||||||
verificationRuntime = runtime
|
}
|
||||||
log.Info("邮箱验证码服务已禁用")
|
return verification.SMTPSettings{
|
||||||
return nil
|
Host: smtpSettings.Relay.RelayHost,
|
||||||
}
|
Port: smtpSettings.Relay.RelayPort,
|
||||||
security, err := smtpSecurityMode(smtpSettings.Relay.Security)
|
Username: smtpSettings.Relay.Username,
|
||||||
if err != nil {
|
Password: smtpSettings.Password,
|
||||||
runtime.Stop()
|
Security: security,
|
||||||
return err
|
From: smtpSettings.Relay.From,
|
||||||
}
|
FromDomain: smtpSettings.Relay.FromDomain,
|
||||||
delivery, err := verification.NewSMTPDelivery(verification.SMTPSettings{
|
}, nil
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
service, err := verification.NewService(runtime.Store, delivery, runtime.Events)
|
||||||
runtime.Stop()
|
|
||||||
return fmt.Errorf("SMTP 投递服务初始化失败: %w", err)
|
|
||||||
}
|
|
||||||
service, err := verification.NewService(runtime.Store, delivery.Delivery(), runtime.Events)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
runtime.Stop()
|
runtime.Stop()
|
||||||
return fmt.Errorf("start verification service: %w", err)
|
return fmt.Errorf("start verification service: %w", err)
|
||||||
@ -348,7 +331,7 @@ func startVerificationRuntime() error {
|
|||||||
controllers.ConfigureSecurityRuntime(service, external, recover, credential)
|
controllers.ConfigureSecurityRuntime(service, external, recover, credential)
|
||||||
credentialService = credential
|
credentialService = credential
|
||||||
verificationRuntime = runtime
|
verificationRuntime = runtime
|
||||||
log.Info("邮箱验证码服务已启动")
|
log.Info("安全验证码服务已启动,外发邮件将在实际使用时检查 SMTP 中继配置")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,9 +357,6 @@ func startWebAuthnRuntime() error {
|
|||||||
}
|
}
|
||||||
snapshot := manager.Snapshot()
|
snapshot := manager.Snapshot()
|
||||||
security := snapshot.Root.Security
|
security := snapshot.Root.Security
|
||||||
if !security.WebAuthnEnabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
service, err := webauthnservice.NewService(db.Instance, webauthnservice.Config{
|
service, err := webauthnservice.NewService(db.Instance, webauthnservice.Config{
|
||||||
RPID: security.WebAuthnRPID,
|
RPID: security.WebAuthnRPID,
|
||||||
RPDisplayName: security.WebAuthnRPDisplayName,
|
RPDisplayName: security.WebAuthnRPDisplayName,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
ErrInvalidSMTPConfig = errors.New("invalid SMTP configuration")
|
ErrInvalidSMTPConfig = errors.New("invalid SMTP configuration")
|
||||||
ErrUnsupportedSMTP = errors.New("unsupported SMTP security mode")
|
ErrUnsupportedSMTP = errors.New("unsupported SMTP security mode")
|
||||||
|
ErrSMTPRelayUnavailable = errors.New("SMTP relay is unavailable")
|
||||||
)
|
)
|
||||||
|
|
||||||
type SMTPSettings struct {
|
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) {
|
func IssueWithSMTP(store *Store, request IssueRequest, settings SMTPSettings) (Challenge, error) {
|
||||||
if store == nil {
|
if store == nil {
|
||||||
return Challenge{}, ErrInvalidChallenge
|
return Challenge{}, ErrInvalidChallenge
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user