wg-portal/internal/app/migrate_v1.go

372 lines
12 KiB
Go
Raw Normal View History

package app
import (
"errors"
"fmt"
"os"
"time"
"github.com/h44z/wg-portal/internal/adapters"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func migrateFromV1(cfg *config.Config, db *gorm.DB, source, typ string) error {
sourceType := config.SupportedDatabase(typ)
switch sourceType {
case config.DatabaseMySQL, config.DatabasePostgres, config.DatabaseMsSQL:
case config.DatabaseSQLite:
if _, err := os.Stat(source); os.IsNotExist(err) {
return fmt.Errorf("invalid source database: %w", err)
}
default:
return errors.New("unsupported database")
}
oldDb, err := adapters.NewDatabase(config.DatabaseConfig{
Type: sourceType,
DSN: source,
})
if err != nil {
return fmt.Errorf("failed to open old database: %w", err)
}
// check if old db is a valid WireGuard Portal v1 database
type DatabaseMigrationInfo struct {
Version string `gorm:"primaryKey"`
Applied time.Time
}
lastVersion := DatabaseMigrationInfo{}
err = oldDb.Order("applied desc, version desc").FirstOrInit(&lastVersion).Error
if err != nil {
return fmt.Errorf("unable to validate old database: %w", err)
}
latestVersion := "1.0.9"
if lastVersion.Version != latestVersion {
return fmt.Errorf("unsupported old version, update to database version %s first: %w", latestVersion, err)
}
logrus.Infof("Found valid V1 database with version: %s", lastVersion.Version)
if err := migrateV1Users(oldDb, db); err != nil {
return fmt.Errorf("user migration failed: %w", err)
}
if err := migrateV1Interfaces(oldDb, db); err != nil {
return fmt.Errorf("user migration failed: %w", err)
}
if err := migrateV1Peers(oldDb, db); err != nil {
return fmt.Errorf("peer migration failed: %w", err)
}
logrus.Infof("Migrated V1 database with version %s, please restart WireGuard Portal", lastVersion.Version)
return nil
}
func migrateV1Users(oldDb, newDb *gorm.DB) error {
type User struct {
Email string `gorm:"primaryKey"`
Source string
IsAdmin bool
Firstname string
Lastname string
Phone string
Password string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
var oldUsers []User
err := oldDb.Find(&oldUsers).Error
if err != nil {
return fmt.Errorf("unable to fetch old user records: %w", err)
}
for _, oldUser := range oldUsers {
var deletionTime *time.Time
deletionReason := ""
if oldUser.DeletedAt.Valid {
delTime := oldUser.DeletedAt.Time
deletionTime = &delTime
deletionReason = "disabled prior to migration"
}
newUser := domain.User{
BaseModel: domain.BaseModel{
2025-01-11 21:56:25 +00:00
CreatedBy: domain.CtxSystemV1Migrator,
UpdatedBy: domain.CtxSystemV1Migrator,
CreatedAt: oldUser.CreatedAt,
UpdatedAt: oldUser.UpdatedAt,
},
Identifier: domain.UserIdentifier(oldUser.Email),
Email: oldUser.Email,
Source: domain.UserSource(oldUser.Source),
ProviderName: "",
IsAdmin: oldUser.IsAdmin,
Firstname: oldUser.Firstname,
Lastname: oldUser.Lastname,
Phone: oldUser.Phone,
Department: "",
Notes: "",
Password: domain.PrivateString(oldUser.Password),
Disabled: deletionTime,
DisabledReason: deletionReason,
Locked: nil,
LockedReason: "",
LinkedPeerCount: 0,
}
if err := newDb.Save(&newUser).Error; err != nil {
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
}
logrus.Debugf(" - User %s migrated", newUser.Identifier)
}
return nil
}
func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
type Device struct {
Type string
DeviceName string `gorm:"primaryKey"`
DisplayName string
PrivateKey string
ListenPort int
FirewallMark uint32
PublicKey string
Mtu int
IPsStr string
DNSStr string
RoutingTable string
PreUp string
PostUp string
PreDown string
PostDown string
SaveConfig bool
DefaultEndpoint string
DefaultAllowedIPsStr string
DefaultPersistentKeepalive int
CreatedAt time.Time
UpdatedAt time.Time
}
var oldDevices []Device
err := oldDb.Find(&oldDevices).Error
if err != nil {
return fmt.Errorf("unable to fetch old device records: %w", err)
}
for _, oldDevice := range oldDevices {
ips, err := domain.CidrsFromString(oldDevice.IPsStr)
if err != nil {
return fmt.Errorf("failed to parse %s ip addresses: %w", oldDevice.DeviceName, err)
}
networks := make([]domain.Cidr, len(ips))
for i, ip := range ips {
networks[i] = domain.CidrFromIpNet(*ip.IpNet())
}
newInterface := domain.Interface{
BaseModel: domain.BaseModel{
2025-01-11 21:56:25 +00:00
CreatedBy: domain.CtxSystemV1Migrator,
UpdatedBy: domain.CtxSystemV1Migrator,
CreatedAt: oldDevice.CreatedAt,
UpdatedAt: oldDevice.UpdatedAt,
},
Identifier: domain.InterfaceIdentifier(oldDevice.DeviceName),
KeyPair: domain.KeyPair{
PrivateKey: oldDevice.PrivateKey,
PublicKey: oldDevice.PublicKey,
},
ListenPort: oldDevice.ListenPort,
Addresses: ips,
DnsStr: "",
DnsSearchStr: "",
Mtu: oldDevice.Mtu,
FirewallMark: oldDevice.FirewallMark,
RoutingTable: oldDevice.RoutingTable,
PreUp: oldDevice.PreUp,
PostUp: oldDevice.PostUp,
PreDown: oldDevice.PreDown,
PostDown: oldDevice.PostDown,
SaveConfig: oldDevice.SaveConfig,
DisplayName: oldDevice.DisplayName,
Type: domain.InterfaceType(oldDevice.Type),
DriverType: "",
Disabled: nil,
DisabledReason: "",
PeerDefNetworkStr: domain.CidrsToString(networks),
PeerDefDnsStr: oldDevice.DNSStr,
PeerDefDnsSearchStr: "",
PeerDefEndpoint: oldDevice.DefaultEndpoint,
PeerDefAllowedIPsStr: oldDevice.DefaultAllowedIPsStr,
PeerDefMtu: oldDevice.Mtu,
PeerDefPersistentKeepalive: oldDevice.DefaultPersistentKeepalive,
PeerDefFirewallMark: 0,
PeerDefRoutingTable: "",
PeerDefPreUp: "",
PeerDefPostUp: "",
PeerDefPreDown: "",
PeerDefPostDown: "",
}
if err := newDb.Save(&newInterface).Error; err != nil {
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
}
logrus.Debugf(" - Interface %s migrated", newInterface.Identifier)
}
return nil
}
func migrateV1Peers(oldDb, newDb *gorm.DB) error {
type Peer struct {
UID string
DeviceName string `gorm:"index"`
Identifier string
Email string `gorm:"index" form:"mail" binding:"required,email"`
IgnoreGlobalSettings bool
PublicKey string `gorm:"primaryKey"`
PresharedKey string
AllowedIPsStr string
AllowedIPsSrvStr string
Endpoint string
PersistentKeepalive int
PrivateKey string
IPsStr string
DNSStr string
Mtu int
DeactivatedAt *time.Time `json:",omitempty"`
DeactivatedReason string `json:",omitempty"`
ExpiresAt *time.Time
CreatedBy string
UpdatedBy string
CreatedAt time.Time
UpdatedAt time.Time
}
var oldPeers []Peer
err := oldDb.Find(&oldPeers).Error
if err != nil {
return fmt.Errorf("unable to fetch old peer records: %w", err)
}
for _, oldPeer := range oldPeers {
ips, err := domain.CidrsFromString(oldPeer.IPsStr)
if err != nil {
return fmt.Errorf("failed to parse %s ip addresses: %w", oldPeer.PublicKey, err)
}
var disableTime *time.Time
disableReason := ""
if oldPeer.DeactivatedAt != nil {
disTime := *oldPeer.DeactivatedAt
disableTime = &disTime
disableReason = oldPeer.DeactivatedReason
}
var expiryTime *time.Time
if oldPeer.ExpiresAt != nil {
expTime := *oldPeer.ExpiresAt
expiryTime = &expTime
}
var iface domain.Interface
var ifaceType domain.InterfaceType
err = newDb.First(&iface, "identifier = ?", oldPeer.DeviceName).Error
if err != nil {
return fmt.Errorf("failed to find interface %s for peer %s: %w", oldPeer.DeviceName, oldPeer.PublicKey, err)
}
switch iface.Type {
case domain.InterfaceTypeClient:
ifaceType = domain.InterfaceTypeServer
case domain.InterfaceTypeServer:
ifaceType = domain.InterfaceTypeClient
case domain.InterfaceTypeAny:
ifaceType = domain.InterfaceTypeAny
}
var user domain.User
err = newDb.First(&user, "identifier = ?",
oldPeer.Email).Error // migrated users use the email address as identifier
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to find user %s for peer %s: %w", oldPeer.Email, oldPeer.PublicKey, err)
}
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
// create dummy user
now := time.Now()
user = domain.User{
BaseModel: domain.BaseModel{
2025-01-11 21:56:25 +00:00
CreatedBy: domain.CtxSystemV1Migrator,
UpdatedBy: domain.CtxSystemV1Migrator,
CreatedAt: now,
UpdatedAt: now,
},
Identifier: domain.UserIdentifier(oldPeer.Email),
Email: oldPeer.Email,
Source: domain.UserSourceDatabase,
ProviderName: "",
IsAdmin: false,
Locked: &now,
LockedReason: domain.DisabledReasonMigrationDummy,
Notes: "created by migration from v1",
}
if err := newDb.Save(&user).Error; err != nil {
return fmt.Errorf("failed to migrate dummy user %s: %w", oldPeer.Email, err)
}
logrus.Debugf(" - Dummy User %s migrated", user.Identifier)
}
newPeer := domain.Peer{
BaseModel: domain.BaseModel{
2025-01-11 21:56:25 +00:00
CreatedBy: domain.CtxSystemV1Migrator,
UpdatedBy: domain.CtxSystemV1Migrator,
CreatedAt: oldPeer.CreatedAt,
UpdatedAt: oldPeer.UpdatedAt,
},
Endpoint: domain.NewConfigOption(oldPeer.Endpoint, !oldPeer.IgnoreGlobalSettings),
EndpointPublicKey: domain.NewConfigOption(iface.PublicKey, !oldPeer.IgnoreGlobalSettings),
AllowedIPsStr: domain.NewConfigOption(oldPeer.AllowedIPsStr, !oldPeer.IgnoreGlobalSettings),
ExtraAllowedIPsStr: oldPeer.AllowedIPsSrvStr,
PresharedKey: domain.PreSharedKey(oldPeer.PresharedKey),
PersistentKeepalive: domain.NewConfigOption(oldPeer.PersistentKeepalive, !oldPeer.IgnoreGlobalSettings),
DisplayName: oldPeer.Identifier,
Identifier: domain.PeerIdentifier(oldPeer.PublicKey),
UserIdentifier: user.Identifier,
InterfaceIdentifier: iface.Identifier,
Disabled: disableTime,
DisabledReason: disableReason,
ExpiresAt: expiryTime,
Notes: "",
Interface: domain.PeerInterfaceConfig{
KeyPair: domain.KeyPair{
PrivateKey: oldPeer.PrivateKey,
PublicKey: oldPeer.PublicKey,
},
Type: ifaceType,
Addresses: ips,
DnsStr: domain.NewConfigOption(oldPeer.DNSStr, !oldPeer.IgnoreGlobalSettings),
DnsSearchStr: domain.NewConfigOption(iface.PeerDefDnsSearchStr, !oldPeer.IgnoreGlobalSettings),
Mtu: domain.NewConfigOption(oldPeer.Mtu, !oldPeer.IgnoreGlobalSettings),
FirewallMark: domain.NewConfigOption(iface.PeerDefFirewallMark, !oldPeer.IgnoreGlobalSettings),
RoutingTable: domain.NewConfigOption(iface.PeerDefRoutingTable, !oldPeer.IgnoreGlobalSettings),
PreUp: domain.NewConfigOption(iface.PeerDefPreUp, !oldPeer.IgnoreGlobalSettings),
PostUp: domain.NewConfigOption(iface.PeerDefPostUp, !oldPeer.IgnoreGlobalSettings),
PreDown: domain.NewConfigOption(iface.PeerDefPreDown, !oldPeer.IgnoreGlobalSettings),
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, !oldPeer.IgnoreGlobalSettings),
},
}
if err := newDb.Save(&newPeer).Error; err != nil {
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
}
logrus.Debugf(" - Peer %s migrated", newPeer.Identifier)
}
return nil
}