From fe3247bdc18c5e74fc554e807be1494259829520 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Fri, 28 Oct 2022 23:21:37 +0200 Subject: [PATCH 1/6] peer expiry feature: database model, frontend updates --- assets/css/custom.css | 10 ++++++++++ assets/tpl/admin_edit_client.html | 12 ++++++++++-- assets/tpl/admin_index.html | 10 ++++++++-- assets/tpl/user_index.html | 5 ++++- internal/common/db.go | 8 ++++++++ internal/common/util.go | 11 ++++++++++- internal/server/api.go | 2 ++ internal/server/handlers_peer.go | 10 +++++++++- internal/server/ldapsync.go | 2 ++ internal/server/server.go | 1 + internal/server/server_helper.go | 8 +++----- internal/server/version.go | 2 +- internal/wireguard/peermanager.go | 27 ++++++++++++++++++++++----- 13 files changed, 90 insertions(+), 18 deletions(-) diff --git a/assets/css/custom.css b/assets/css/custom.css index 5a6294c..ae0b24f 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -73,6 +73,10 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768 color: #d03131; } +.expiring-peer { + color: #d09d12; +} + .tokenfield .token { border-radius: 0px; border: 1px solid #1a1a1a; @@ -105,4 +109,10 @@ a.advanced-settings.collapsed:before { .text-blue { color: #0057bb; +} + +@media (min-width: 992px) { + .pull-right-lg { + float: right; + } } \ No newline at end of file diff --git a/assets/tpl/admin_edit_client.html b/assets/tpl/admin_edit_client.html index 0b85284..d7d5cbf 100644 --- a/assets/tpl/admin_edit_client.html +++ b/assets/tpl/admin_edit_client.html @@ -106,7 +106,7 @@
-
+
+
+ + +
@@ -185,7 +189,7 @@
-
+
+
+ + +
diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html index 8a3c572..e000068 100644 --- a/assets/tpl/admin_index.html +++ b/assets/tpl/admin_index.html @@ -170,7 +170,7 @@ - {{$p.Identifier}} + {{$p.Identifier}}{{if $p.WillExpire}} {{end}} {{$p.PublicKey}} {{if eq $.Device.Type "server"}} {{$p.Email}} @@ -243,8 +243,14 @@ {{end}}
+ {{if $p.DeactivatedAt}} +
Peer is disabled!
+ {{end}} + {{if $p.WillExpire}}{{if not $p.DeactivatedAt}} +
Peer will expire on {{ formatDate $p.ExpiresAt}}
+ {{end}}{{end}} {{if eq $.Device.Type "server"}} -
+ diff --git a/assets/tpl/user_index.html b/assets/tpl/user_index.html index f0e53d7..619fe19 100644 --- a/assets/tpl/user_index.html +++ b/assets/tpl/user_index.html @@ -102,7 +102,10 @@
-
+ {{if $p.DeactivatedAt}} +
Peer is disabled!
+ {{end}} + diff --git a/internal/common/db.go b/internal/common/db.go index d8122be..9e8f403 100644 --- a/internal/common/db.go +++ b/internal/common/db.go @@ -36,6 +36,14 @@ func init() { return nil }, }) + + migrations = append(migrations, Migration{ + version: "1.0.9", + migrateFn: func(db *gorm.DB) error { + logrus.Infof("upgraded database format to version 1.0.9") + return nil + }, + }) } type SupportedDatabase string diff --git a/internal/common/util.go b/internal/common/util.go index bbde700..e33ebf9 100644 --- a/internal/common/util.go +++ b/internal/common/util.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "strings" + "time" ) // BroadcastAddr returns the last address in the given network, or the broadcast address. @@ -21,7 +22,7 @@ func BroadcastAddr(n *net.IPNet) net.IP { return broadcast } -// http://play.golang.org/p/m8TNTtygK0 +// http://play.golang.org/p/m8TNTtygK0 func IncreaseIP(ip net.IP) { for j := len(ip) - 1; j >= 0; j-- { ip[j]++ @@ -84,3 +85,11 @@ func ByteCountSI(b int64) string { return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } + +func FormatDateHTML(t *time.Time) string { + if t == nil { + return "" + } + + return t.Format("2006-01-02") +} diff --git a/internal/server/api.go b/internal/server/api.go index 11f3be0..0bfb471 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -439,6 +439,7 @@ func (s *ApiServer) PutPeer(c *gin.Context) { now := time.Now() if updatePeer.DeactivatedAt != nil { updatePeer.DeactivatedAt = &now + updatePeer.DeactivatedReason = "api update" } if err := s.s.UpdatePeer(updatePeer, now); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) @@ -516,6 +517,7 @@ func (s *ApiServer) PatchPeer(c *gin.Context) { now := time.Now() if mergedPeer.DeactivatedAt != nil { mergedPeer.DeactivatedAt = &now + mergedPeer.DeactivatedReason = "api update" } if err := s.s.UpdatePeer(mergedPeer, now); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) diff --git a/internal/server/handlers_peer.go b/internal/server/handlers_peer.go index 78966ee..9e87e5b 100644 --- a/internal/server/handlers_peer.go +++ b/internal/server/handlers_peer.go @@ -71,8 +71,13 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) { now := time.Now() if disabled && currentPeer.DeactivatedAt == nil { formPeer.DeactivatedAt = &now + formPeer.DeactivatedReason = "admin update" } else if !disabled { formPeer.DeactivatedAt = nil + formPeer.DeactivatedReason = "" + } + if formPeer.ExpiresAt != nil && formPeer.ExpiresAt.IsZero() { + formPeer.ExpiresAt = nil } // Update in database @@ -129,6 +134,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) { now := time.Now() if disabled { formPeer.DeactivatedAt = &now + formPeer.DeactivatedReason = "admin create" } if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { @@ -189,7 +195,7 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) { logrus.Infof("creating %d ldap peers", len(emails)) for i := range emails { - if err := s.CreatePeerByEmail(currentSession.DeviceName, emails[i], formData.Identifier, false); err != nil { + if err := s.CreatePeerByEmail(currentSession.DeviceName, emails[i], formData.Identifier); err != nil { _ = s.updateFormInSession(c, formData) SetFlashMessage(c, "failed to add user: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create") @@ -440,6 +446,7 @@ func (s *Server) PostUserCreatePeer(c *gin.Context) { now := time.Now() if disabled { formPeer.DeactivatedAt = &now + formPeer.DeactivatedReason = "user create" } if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { @@ -496,6 +503,7 @@ func (s *Server) PostUserEditPeer(c *gin.Context) { now := time.Now() if disabled && currentPeer.DeactivatedAt == nil { currentPeer.DeactivatedAt = &now + currentPeer.DeactivatedReason = "user update" } // Update in database diff --git a/internal/server/ldapsync.go b/internal/server/ldapsync.go index 9bef96d..6cee9c0 100644 --- a/internal/server/ldapsync.go +++ b/internal/server/ldapsync.go @@ -112,6 +112,7 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) { for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) { now := time.Now() peer.DeactivatedAt = &now + peer.DeactivatedReason = "missing ldap user" if err := s.UpdatePeer(peer, now); err != nil { logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err) } @@ -141,6 +142,7 @@ func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) { for _, peer := range s.peers.GetPeersByMail(user.Email) { now := time.Now() peer.DeactivatedAt = nil + peer.DeactivatedReason = "" if err = s.UpdatePeer(peer, now); err != nil { logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err) } diff --git a/internal/server/server.go b/internal/server/server.go index a22e84d..7968bd0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -127,6 +127,7 @@ func (s *Server) Setup(ctx context.Context) error { }) s.server.Use(sessions.Sessions("authsession", cookieStore)) s.server.SetFuncMap(template.FuncMap{ + "formatDate": common.FormatDateHTML, "formatBytes": common.ByteCountSI, "urlEncode": url.QueryEscape, "startsWith": strings.HasPrefix, diff --git a/internal/server/server_helper.go b/internal/server/server_helper.go index f366396..71582d1 100644 --- a/internal/server/server_helper.go +++ b/internal/server/server_helper.go @@ -62,7 +62,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) { } // CreatePeerByEmail creates a new peer for the given email. -func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disabled bool) error { +func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string) error { user := s.users.GetUser(email) peer, err := s.PrepareNewPeer(device) @@ -75,10 +75,6 @@ func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disab } else { peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix) } - now := time.Now() - if disabled { - peer.DeactivatedAt = &now - } return s.CreatePeer(device, peer) } @@ -281,6 +277,7 @@ func (s *Server) UpdateUser(user users.User) error { for _, peer := range s.peers.GetPeersByMail(user.Email) { now := time.Now() peer.DeactivatedAt = nil + peer.DeactivatedReason = "" if err := s.UpdatePeer(peer, now); err != nil { logrus.Errorf("failed to update (re)activated peer %s for %s: %v", peer.PublicKey, user.Email, err) } @@ -302,6 +299,7 @@ func (s *Server) DeleteUser(user users.User) error { for _, peer := range s.peers.GetPeersByMail(user.Email) { now := time.Now() peer.DeactivatedAt = &now + peer.DeactivatedReason = "user deleted" if err := s.UpdatePeer(peer, now); err != nil { logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err) } diff --git a/internal/server/version.go b/internal/server/version.go index a1b0048..58fdf31 100644 --- a/internal/server/version.go +++ b/internal/server/version.go @@ -1,4 +1,4 @@ package server var Version = "testbuild" -var DatabaseVersion = "1.0.8" +var DatabaseVersion = "1.0.9" diff --git a/internal/wireguard/peermanager.go b/internal/wireguard/peermanager.go index 504b9a4..3b4e504 100644 --- a/internal/wireguard/peermanager.go +++ b/internal/wireguard/peermanager.go @@ -108,11 +108,15 @@ type Peer struct { // Global Device Settings (can be ignored, only make sense if device is in server mode) Mtu int `form:"mtu" binding:"gte=0,lte=1500"` - DeactivatedAt *time.Time `json:",omitempty"` - CreatedBy string - UpdatedBy string - CreatedAt time.Time - UpdatedAt time.Time + DeactivatedAt *time.Time `json:",omitempty"` + DeactivatedReason string `json:",omitempty"` + + ExpiresAt *time.Time `json:",omitempty" form:"expires_at" binding:"omitempty" time_format:"2006-01-02"` + + CreatedBy string + UpdatedBy string + CreatedAt time.Time + UpdatedAt time.Time } func (p *Peer) SetIPAddresses(addresses ...string) { @@ -238,6 +242,19 @@ func (p Peer) IsValid() bool { return true } +func (p Peer) WillExpire() bool { + if p.ExpiresAt == nil { + return false + } + if p.DeactivatedAt != nil { + return false // already deactivated... + } + if p.ExpiresAt.After(time.Now()) { + return true + } + return false +} + func (p Peer) GetConfigFileName() string { reg := regexp.MustCompile("[^a-zA-Z0-9_-]+") return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf" From 0d5b895174494f8d1224ea374798f767a10744a9 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 29 Oct 2022 10:06:58 +0200 Subject: [PATCH 2/6] lazy load qr code (if browser supports it) --- assets/tpl/admin_index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html index e000068..7ab4dc9 100644 --- a/assets/tpl/admin_index.html +++ b/assets/tpl/admin_index.html @@ -239,7 +239,7 @@
{{if eq $.Device.Type "server"}} - + Configuration QR Code {{end}}
From 6f4af97024291a21d1fa1335d8112e4d99095d02 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 29 Oct 2022 10:12:42 +0200 Subject: [PATCH 3/6] peer expiry feature: frontend updates --- assets/tpl/admin_index.html | 4 ++-- assets/tpl/user_index.html | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html index 7ab4dc9..f4e5544 100644 --- a/assets/tpl/admin_index.html +++ b/assets/tpl/admin_index.html @@ -246,9 +246,9 @@ {{if $p.DeactivatedAt}}
Peer is disabled!
{{end}} - {{if $p.WillExpire}}{{if not $p.DeactivatedAt}} + {{if $p.WillExpire}}
Peer will expire on {{ formatDate $p.ExpiresAt}}
- {{end}}{{end}} + {{end}} {{if eq $.Device.Type "server"}}
Download diff --git a/assets/tpl/user_index.html b/assets/tpl/user_index.html index 619fe19..4da4fae 100644 --- a/assets/tpl/user_index.html +++ b/assets/tpl/user_index.html @@ -49,7 +49,7 @@ - {{$p.Identifier}} + {{$p.Identifier}}{{if $p.WillExpire}} {{end}} {{$p.PublicKey}} {{$p.Email}} {{$p.IPsStr}} @@ -105,6 +105,9 @@ {{if $p.DeactivatedAt}}
Peer is disabled!
{{end}} + {{if $p.WillExpire}} +
Profile expires on {{ formatDate $p.ExpiresAt}}
+ {{end}}
Download Email From 4a0e773d969c71fb16b7da6570c56b3b0f33a21e Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 29 Oct 2022 11:21:04 +0200 Subject: [PATCH 4/6] peer expiry feature: expiration check --- internal/server/configuration.go | 6 ++- internal/server/handlers_interface.go | 2 +- internal/server/server.go | 2 + internal/server/server_helper.go | 60 ++++++++++++++++++++++++++- internal/wireguard/peermanager.go | 14 +++++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/internal/server/configuration.go b/internal/server/configuration.go index f904dbb..ec5a1cb 100644 --- a/internal/server/configuration.go +++ b/internal/server/configuration.go @@ -67,10 +67,11 @@ type Config struct { EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"` CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"` SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"` - WGExoprterFriendlyNames bool `yaml:"wgExporterFriendlyNames" envconfig:"WG_EXPORTER_FRIENDLY_NAMES"` + WGExporterFriendlyNames bool `yaml:"wgExporterFriendlyNames" envconfig:"WG_EXPORTER_FRIENDLY_NAMES"` LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"` SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"` LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"` + BackgroundTaskInterval int `yaml:"backgroundTaskInterval" envconfig:"BACKGROUND_TASK_INTERVAL"` // in seconds } `yaml:"core"` Database common.DatabaseConfig `yaml:"database"` Email common.MailConfig `yaml:"email"` @@ -92,8 +93,9 @@ func NewConfig() *Config { cfg.Core.AdminPassword = "wgportal" cfg.Core.LdapEnabled = false cfg.Core.EditableKeys = true - cfg.Core.WGExoprterFriendlyNames = false + cfg.Core.WGExporterFriendlyNames = false cfg.Core.SessionSecret = "secret" + cfg.Core.BackgroundTaskInterval = 15 * 60 // 15 minutes cfg.Database.Typ = "sqlite" cfg.Database.Database = "data/wg_portal.db" diff --git a/internal/server/handlers_interface.go b/internal/server/handlers_interface.go index 72f5a6f..ca6804d 100644 --- a/internal/server/handlers_interface.go +++ b/internal/server/handlers_interface.go @@ -112,7 +112,7 @@ func (s *Server) GetInterfaceConfig(c *gin.Context) { currentSession := GetSessionData(c) device := s.peers.GetDevice(currentSession.DeviceName) peers := s.peers.GetActivePeers(device.DeviceName) - cfg, err := device.GetConfigFile(peers, s.config.Core.WGExoprterFriendlyNames) + cfg, err := device.GetConfigFile(peers, s.config.Core.WGExporterFriendlyNames) if err != nil { s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) return diff --git a/internal/server/server.go b/internal/server/server.go index 7968bd0..06cfeaa 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -216,6 +216,8 @@ func (s *Server) Run() { go s.SyncLdapWithUserDatabase() } + go s.RunBackgroundTasks(s.ctx) + // Run web service srv := &http.Server{ Addr: s.config.Core.ListeningAddress, diff --git a/internal/server/server_helper.go b/internal/server/server_helper.go index 71582d1..27914ab 100644 --- a/internal/server/server_helper.go +++ b/internal/server/server_helper.go @@ -1,6 +1,7 @@ package server import ( + "context" "crypto/md5" "fmt" "io/ioutil" @@ -205,7 +206,7 @@ func (s *Server) WriteWireGuardConfigFile(device string) error { } dev := s.peers.GetDevice(device) - cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device), s.config.Core.WGExoprterFriendlyNames) + cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device), s.config.Core.WGExporterFriendlyNames) if err != nil { return errors.WithMessage(err, "failed to get config file") } @@ -374,3 +375,60 @@ func (s *Server) GetDeviceNames() map[string]string { return devNames } + +func (s *Server) RunBackgroundTasks(ctx context.Context) { + running := true + for running { + select { + case <-ctx.Done(): + running = false + continue + case <-time.After(time.Duration(s.config.Core.BackgroundTaskInterval) * time.Second): + // sleep completed, select will stop blocking + } + + logrus.Debug("running periodic background tasks...") + + err := s.checkExpiredPeers() + if err != nil { + logrus.Errorf("failed to check expired peers: %v", err) + } + } +} + +func (s *Server) checkExpiredPeers() error { + now := time.Now() + + for _, devName := range s.wg.Cfg.DeviceNames { + changed := false + peers := s.peers.GetAllPeers(devName) + for _, peer := range peers { + if peer.IsExpired() && !peer.IsDeactivated() { + changed = true + + peer.UpdatedAt = now + peer.DeactivatedAt = &now + peer.DeactivatedReason = "expired" + + res := s.db.Save(&peer) + if res.Error != nil { + return fmt.Errorf("failed save expired peer %s: %w", peer.PublicKey, res.Error) + } + + err := s.wg.RemovePeer(peer.DeviceName, peer.PublicKey) + if err != nil { + return fmt.Errorf("failed to expire peer %s: %w", peer.PublicKey, err) + } + } + } + + if changed { + err := s.WriteWireGuardConfigFile(devName) + if err != nil { + return fmt.Errorf("failed to persist config for interface %s: %w", devName, err) + } + } + } + + return nil +} diff --git a/internal/wireguard/peermanager.go b/internal/wireguard/peermanager.go index 3b4e504..f2cd108 100644 --- a/internal/wireguard/peermanager.go +++ b/internal/wireguard/peermanager.go @@ -255,6 +255,20 @@ func (p Peer) WillExpire() bool { return false } +func (p Peer) IsExpired() bool { + if p.ExpiresAt == nil { + return false + } + if p.ExpiresAt.Before(time.Now()) { + return true + } + return false +} + +func (p Peer) IsDeactivated() bool { + return p.DeactivatedAt != nil +} + func (p Peer) GetConfigFileName() string { reg := regexp.MustCompile("[^a-zA-Z0-9_-]+") return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf" From c43e8d7ca2410a4f6e6e0d6425756ba1f8a8f0f5 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 29 Oct 2022 13:03:05 +0200 Subject: [PATCH 5/6] peer expiry feature: re-activate expired peers --- internal/server/api.go | 4 ++-- internal/server/handlers_peer.go | 15 ++++++++++----- internal/server/ldapsync.go | 4 +++- internal/server/server_helper.go | 4 ++-- internal/wireguard/peermanager.go | 12 ++++++++++++ 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/internal/server/api.go b/internal/server/api.go index 0bfb471..a4fab6f 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -439,7 +439,7 @@ func (s *ApiServer) PutPeer(c *gin.Context) { now := time.Now() if updatePeer.DeactivatedAt != nil { updatePeer.DeactivatedAt = &now - updatePeer.DeactivatedReason = "api update" + updatePeer.DeactivatedReason = wireguard.DeactivatedReasonApiEdit } if err := s.s.UpdatePeer(updatePeer, now); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) @@ -517,7 +517,7 @@ func (s *ApiServer) PatchPeer(c *gin.Context) { now := time.Now() if mergedPeer.DeactivatedAt != nil { mergedPeer.DeactivatedAt = &now - mergedPeer.DeactivatedReason = "api update" + mergedPeer.DeactivatedReason = wireguard.DeactivatedReasonApiEdit } if err := s.s.UpdatePeer(mergedPeer, now); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) diff --git a/internal/server/handlers_peer.go b/internal/server/handlers_peer.go index 9e87e5b..0037429 100644 --- a/internal/server/handlers_peer.go +++ b/internal/server/handlers_peer.go @@ -71,12 +71,17 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) { now := time.Now() if disabled && currentPeer.DeactivatedAt == nil { formPeer.DeactivatedAt = &now - formPeer.DeactivatedReason = "admin update" + formPeer.DeactivatedReason = wireguard.DeactivatedReasonAdminEdit } else if !disabled { formPeer.DeactivatedAt = nil formPeer.DeactivatedReason = "" + // If a peer was deactivated due to expiry, remove the expires-at date to avoid + // unwanted re-expiry. + if currentPeer.DeactivatedReason == wireguard.DeactivatedReasonExpired { + formPeer.ExpiresAt = nil + } } - if formPeer.ExpiresAt != nil && formPeer.ExpiresAt.IsZero() { + if formPeer.ExpiresAt != nil && formPeer.ExpiresAt.IsZero() { // convert 01-01-0001 to nil formPeer.ExpiresAt = nil } @@ -134,7 +139,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) { now := time.Now() if disabled { formPeer.DeactivatedAt = &now - formPeer.DeactivatedReason = "admin create" + formPeer.DeactivatedReason = wireguard.DeactivatedReasonAdminCreate } if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { @@ -446,7 +451,7 @@ func (s *Server) PostUserCreatePeer(c *gin.Context) { now := time.Now() if disabled { formPeer.DeactivatedAt = &now - formPeer.DeactivatedReason = "user create" + formPeer.DeactivatedReason = wireguard.DeactivatedReasonUserCreate } if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { @@ -503,7 +508,7 @@ func (s *Server) PostUserEditPeer(c *gin.Context) { now := time.Now() if disabled && currentPeer.DeactivatedAt == nil { currentPeer.DeactivatedAt = &now - currentPeer.DeactivatedReason = "user update" + currentPeer.DeactivatedReason = wireguard.DeactivatedReasonUserEdit } // Update in database diff --git a/internal/server/ldapsync.go b/internal/server/ldapsync.go index 6cee9c0..bad4cab 100644 --- a/internal/server/ldapsync.go +++ b/internal/server/ldapsync.go @@ -4,6 +4,8 @@ import ( "strings" "time" + "github.com/h44z/wg-portal/internal/wireguard" + "github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/users" "github.com/sirupsen/logrus" @@ -112,7 +114,7 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) { for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) { now := time.Now() peer.DeactivatedAt = &now - peer.DeactivatedReason = "missing ldap user" + peer.DeactivatedReason = wireguard.DeactivatedReasonLdapMissing if err := s.UpdatePeer(peer, now); err != nil { logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err) } diff --git a/internal/server/server_helper.go b/internal/server/server_helper.go index 27914ab..f285847 100644 --- a/internal/server/server_helper.go +++ b/internal/server/server_helper.go @@ -300,7 +300,7 @@ func (s *Server) DeleteUser(user users.User) error { for _, peer := range s.peers.GetPeersByMail(user.Email) { now := time.Now() peer.DeactivatedAt = &now - peer.DeactivatedReason = "user deleted" + peer.DeactivatedReason = wireguard.DeactivatedReasonUserMissing if err := s.UpdatePeer(peer, now); err != nil { logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err) } @@ -408,7 +408,7 @@ func (s *Server) checkExpiredPeers() error { peer.UpdatedAt = now peer.DeactivatedAt = &now - peer.DeactivatedReason = "expired" + peer.DeactivatedReason = wireguard.DeactivatedReasonExpired res := s.db.Save(&peer) if res.Error != nil { diff --git a/internal/wireguard/peermanager.go b/internal/wireguard/peermanager.go index f2cd108..30f4cf9 100644 --- a/internal/wireguard/peermanager.go +++ b/internal/wireguard/peermanager.go @@ -23,6 +23,18 @@ import ( "gorm.io/gorm" ) +const ( + DeactivatedReasonExpired = "expired" + DeactivatedReasonUserEdit = "user edit action" + DeactivatedReasonUserCreate = "user create action" + DeactivatedReasonAdminEdit = "admin edit action" + DeactivatedReasonAdminCreate = "admin create action" + DeactivatedReasonApiEdit = "api edit action" + DeactivatedReasonApiCreate = "api create action" + DeactivatedReasonLdapMissing = "missing in ldap" + DeactivatedReasonUserMissing = "missing user" +) + // CUSTOM VALIDATORS ---------------------------------------------------------------------------- var cidrList validator.Func = func(fl validator.FieldLevel) bool { cidrListStr := fl.Field().String() From 0f338718506040fd2cba4e53726158f874d67c46 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 29 Oct 2022 13:18:32 +0200 Subject: [PATCH 6/6] peer expiry feature: update api docs and readme --- Makefile | 2 +- README.md | 113 +++++++++++++++++---------------- internal/server/docs/docs.go | 119 +++++++++++++++-------------------- 3 files changed, 109 insertions(+), 125 deletions(-) diff --git a/Makefile b/Makefile index 06e371c..6c37492 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ docker-push: docker push $(IMAGE) api-docs: - cd internal/server; swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo api.go + cd internal; swag init --propertyStrategy pascalcase --parseInternal --generalInfo server/api.go --output server/docs/ $(GOCMD) fmt internal/server/docs/docs.go $(BUILDDIR)/%-amd64: cmd/%/main.go dep phony diff --git a/README.md b/README.md index 269cc83..19efb90 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider. * Can be used with existing WireGuard setups * Support for multiple WireGuard interfaces * REST API for management and client deployment + * Peer Expiry Feature ![Screenshot](screenshot.png) @@ -108,61 +109,63 @@ For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`. ### Configuration Options The following configuration options are available: -| environment | yaml | yaml_parent | default_value | description | -|----------------------------|-------------------------|-------------|-------------------------------------------------|-------------------------------------------------------------------------------------------| -| LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. | -| EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. | -| WEBSITE_TITLE | title | core | WireGuard VPN | The website title. | -| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). | -| MAIL_FROM | mailFrom | core | WireGuard VPN | The email address from which emails are sent. | -| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. | -| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. | -| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. | -| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. | -| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. | -| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. | -| WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). | -| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. | -| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. | -| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. | -| DATABASE_HOST | host | database | | The mysql server address. | -| DATABASE_PORT | port | database | | The mysql server port. | -| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. | -| DATABASE_USERNAME | user | database | | The mysql user. | -| DATABASE_PASSWORD | password | database | | The mysql password. | -| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. | -| EMAIL_PORT | port | email | 25 | The email server port. | -| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. | -| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. | -| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. | -| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. | -| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. | -| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. | -| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. | -| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). | -| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: .conf. | -| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. | -| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. | -| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. | -| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. | -| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. | -| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. | -| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. | -| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. | -| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. | -| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. | -| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. | -| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. | -| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. | -| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. | -| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. | -| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password | -| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path | -| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path | -| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. | -| LOG_JSON | | | false | Format log output as JSON. | -| LOG_COLOR | | | true | Colorize log output. | -| CONFIG_FILE | | | config.yml | The config file path. | +| environment | yaml | yaml_parent | default_value | description | +|----------------------------|-------------------------|-------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. | +| EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. | +| WEBSITE_TITLE | title | core | WireGuard VPN | The website title. | +| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). | +| MAIL_FROM | mailFrom | core | WireGuard VPN | The email address from which emails are sent. | +| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. | +| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. | +| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. | +| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. | +| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. | +| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. | +| WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). | +| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. | +| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. | +| BACKGROUND_TASK_INTERVAL | backgroundTaskInterval | core | 900 | The interval (in seconds) for the background tasks (like peer expiry check). | +| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. | +| DATABASE_HOST | host | database | | The mysql server address. | +| DATABASE_PORT | port | database | | The mysql server port. | +| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. | +| DATABASE_USERNAME | user | database | | The mysql user. | +| DATABASE_PASSWORD | password | database | | The mysql password. | +| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. | +| EMAIL_PORT | port | email | 25 | The email server port. | +| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. | +| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. | +| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. | +| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. | +| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. | +| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. | +| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. | +| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). | +| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: .conf. | +| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. | +| USER_MANAGE_PEERS | userManagePeers | wg | false | Logged in user can create or update peers (partially). | +| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. | +| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. | +| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. | +| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. | +| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. | +| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. | +| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. | +| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. | +| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. | +| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. | +| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. | +| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. | +| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. | +| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. | +| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password | +| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path | +| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path | +| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. | +| LOG_JSON | | | false | Format log output as JSON. | +| LOG_COLOR | | | true | Colorize log output. | +| CONFIG_FILE | | | config.yml | The config file path. | ### Sample yaml configuration config.yml: diff --git a/internal/server/docs/docs.go b/internal/server/docs/docs.go index 6b4b029..7fd5fd8 100644 --- a/internal/server/docs/docs.go +++ b/internal/server/docs/docs.go @@ -1,17 +1,10 @@ -// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// Package docs GENERATED BY SWAG; DO NOT EDIT // This file was generated by swaggo/swag package docs -import ( - "bytes" - "encoding/json" - "strings" - "text/template" +import "github.com/swaggo/swag" - "github.com/swaggo/swag" -) - -var doc = `{ +const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { @@ -1267,10 +1260,13 @@ var doc = `{ "type": "string" }, "Mtu": { - "type": "integer" + "type": "integer", + "maximum": 1500, + "minimum": 0 }, "PersistentKeepalive": { - "type": "integer" + "type": "integer", + "minimum": 0 } } }, @@ -1344,16 +1340,19 @@ var doc = `{ "type": "string" }, "DefaultPersistentKeepalive": { - "type": "integer" + "type": "integer", + "minimum": 0 }, "DeviceName": { "type": "string" }, "DisplayName": { - "type": "string" + "type": "string", + "maxLength": 200 }, "FirewallMark": { - "type": "integer" + "type": "integer", + "minimum": 0 }, "IPsStr": { "description": "comma separated list of the IPs of the client, wg-quick addition", @@ -1364,7 +1363,9 @@ var doc = `{ }, "Mtu": { "description": "the interface MTU, wg-quick addition", - "type": "integer" + "type": "integer", + "maximum": 1500, + "minimum": 0 }, "PostDown": { "description": "post down script, wg-quick addition", @@ -1399,7 +1400,11 @@ var doc = `{ "type": "boolean" }, "Type": { - "type": "string" + "type": "string", + "enum": [ + "client", + "server" + ] }, "UpdatedAt": { "type": "string" @@ -1438,11 +1443,18 @@ var doc = `{ "DeactivatedAt": { "type": "string" }, + "DeactivatedReason": { + "type": "string" + }, "DeviceName": { "type": "string" }, "DeviceType": { - "type": "string" + "type": "string", + "enum": [ + "client", + "server" + ] }, "Email": { "type": "string" @@ -1450,23 +1462,30 @@ var doc = `{ "Endpoint": { "type": "string" }, + "ExpiresAt": { + "type": "string" + }, "IPsStr": { "description": "a comma separated list of IPs of the client", "type": "string" }, "Identifier": { "description": "Identifier AND Email make a WireGuard peer unique", - "type": "string" + "type": "string", + "maxLength": 64 }, "IgnoreGlobalSettings": { "type": "boolean" }, "Mtu": { "description": "Global Device Settings (can be ignored, only make sense if device is in server mode)", - "type": "integer" + "type": "integer", + "maximum": 1500, + "minimum": 0 }, "PersistentKeepalive": { - "type": "integer" + "type": "integer", + "minimum": 0 }, "PresharedKey": { "type": "string" @@ -1502,56 +1521,18 @@ var doc = `{ } }` -type swaggerInfo struct { - Version string - Host string - BasePath string - Schemes []string - Title string - Description string -} - // SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = swaggerInfo{ - Version: "1.0", - Host: "", - BasePath: "/api/v1", - Schemes: []string{}, - Title: "WireGuard Portal API", - Description: "WireGuard Portal API for managing users and peers.", -} - -type s struct{} - -func (s *s) ReadDoc() string { - sInfo := SwaggerInfo - sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) - - t, err := template.New("swagger_info").Funcs(template.FuncMap{ - "marshal": func(v interface{}) string { - a, _ := json.Marshal(v) - return string(a) - }, - "escape": func(v interface{}) string { - // escape tabs - str := strings.Replace(v.(string), "\t", "\\t", -1) - // replace " with \", and if that results in \\", replace that with \\\" - str = strings.Replace(str, "\"", "\\\"", -1) - return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) - }, - }).Parse(doc) - if err != nil { - return doc - } - - var tpl bytes.Buffer - if err := t.Execute(&tpl, sInfo); err != nil { - return doc - } - - return tpl.String() +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "WireGuard Portal API", + Description: "WireGuard Portal API for managing users and peers.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, } func init() { - swag.Register(swag.Name, &s{}) + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) }