peer expiry feature: database model, frontend updates

This commit is contained in:
Christoph Haas 2022-10-28 23:21:37 +02:00
parent e4b927bc45
commit fe3247bdc1
13 changed files with 90 additions and 18 deletions

View File

@ -73,6 +73,10 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768
color: #d03131; color: #d03131;
} }
.expiring-peer {
color: #d09d12;
}
.tokenfield .token { .tokenfield .token {
border-radius: 0px; border-radius: 0px;
border: 1px solid #1a1a1a; border: 1px solid #1a1a1a;
@ -105,4 +109,10 @@ a.advanced-settings.collapsed:before {
.text-blue { .text-blue {
color: #0057bb; color: #0057bb;
}
@media (min-width: 992px) {
.pull-right-lg {
float: right;
}
} }

View File

@ -106,7 +106,7 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-6">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}> <input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="server_Disabled"> <label class="custom-control-label" for="server_Disabled">
@ -120,6 +120,10 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div> </div>
@ -185,7 +189,7 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-6">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="client_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}> <input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="client_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="client_Disabled"> <label class="custom-control-label" for="client_Disabled">
@ -193,6 +197,10 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div> </div>

View File

@ -170,7 +170,7 @@
<!-- online check --> <!-- online check -->
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span> <span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
</th> </th>
<td>{{$p.Identifier}}</td> <td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
<td>{{$p.PublicKey}}</td> <td>{{$p.PublicKey}}</td>
{{if eq $.Device.Type "server"}} {{if eq $.Device.Type "server"}}
<td>{{$p.Email}}</td> <td>{{$p.Email}}</td>
@ -243,8 +243,14 @@
{{end}} {{end}}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
{{if $p.WillExpire}}{{if not $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Peer will expire on {{ formatDate $p.ExpiresAt}}</div>
{{end}}{{end}}
{{if eq $.Device.Type "server"}} {{if eq $.Device.Type "server"}}
<div class="float-right mt-5"> <div class="pull-right-lg mt-lg-5 mt-md-3">
<a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a> <a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a> <a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div> </div>

View File

@ -102,7 +102,10 @@
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/> <img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="float-right mt-5"> {{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
<div class="pull-right-lg mt-lg-5 mt-md-3">
<a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a> <a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a> <a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div> </div>

View File

@ -36,6 +36,14 @@ func init() {
return nil 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 type SupportedDatabase string

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
) )
// BroadcastAddr returns the last address in the given network, or the broadcast address. // 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 return broadcast
} }
// http://play.golang.org/p/m8TNTtygK0 // http://play.golang.org/p/m8TNTtygK0
func IncreaseIP(ip net.IP) { func IncreaseIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- { for j := len(ip) - 1; j >= 0; j-- {
ip[j]++ ip[j]++
@ -84,3 +85,11 @@ func ByteCountSI(b int64) string {
return fmt.Sprintf("%.1f %cB", return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp]) float64(b)/float64(div), "kMGTPE"[exp])
} }
func FormatDateHTML(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("2006-01-02")
}

View File

@ -439,6 +439,7 @@ func (s *ApiServer) PutPeer(c *gin.Context) {
now := time.Now() now := time.Now()
if updatePeer.DeactivatedAt != nil { if updatePeer.DeactivatedAt != nil {
updatePeer.DeactivatedAt = &now updatePeer.DeactivatedAt = &now
updatePeer.DeactivatedReason = "api update"
} }
if err := s.s.UpdatePeer(updatePeer, now); err != nil { if err := s.s.UpdatePeer(updatePeer, now); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
@ -516,6 +517,7 @@ func (s *ApiServer) PatchPeer(c *gin.Context) {
now := time.Now() now := time.Now()
if mergedPeer.DeactivatedAt != nil { if mergedPeer.DeactivatedAt != nil {
mergedPeer.DeactivatedAt = &now mergedPeer.DeactivatedAt = &now
mergedPeer.DeactivatedReason = "api update"
} }
if err := s.s.UpdatePeer(mergedPeer, now); err != nil { if err := s.s.UpdatePeer(mergedPeer, now); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})

View File

@ -71,8 +71,13 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
now := time.Now() now := time.Now()
if disabled && currentPeer.DeactivatedAt == nil { if disabled && currentPeer.DeactivatedAt == nil {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = "admin update"
} else if !disabled { } else if !disabled {
formPeer.DeactivatedAt = nil formPeer.DeactivatedAt = nil
formPeer.DeactivatedReason = ""
}
if formPeer.ExpiresAt != nil && formPeer.ExpiresAt.IsZero() {
formPeer.ExpiresAt = nil
} }
// Update in database // Update in database
@ -129,6 +134,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
now := time.Now() now := time.Now()
if disabled { if disabled {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = "admin create"
} }
if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { 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)) logrus.Infof("creating %d ldap peers", len(emails))
for i := range 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) _ = s.updateFormInSession(c, formData)
SetFlashMessage(c, "failed to add user: "+err.Error(), "danger") SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create")
@ -440,6 +446,7 @@ func (s *Server) PostUserCreatePeer(c *gin.Context) {
now := time.Now() now := time.Now()
if disabled { if disabled {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = "user create"
} }
if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil {
@ -496,6 +503,7 @@ func (s *Server) PostUserEditPeer(c *gin.Context) {
now := time.Now() now := time.Now()
if disabled && currentPeer.DeactivatedAt == nil { if disabled && currentPeer.DeactivatedAt == nil {
currentPeer.DeactivatedAt = &now currentPeer.DeactivatedAt = &now
currentPeer.DeactivatedReason = "user update"
} }
// Update in database // Update in database

View File

@ -112,6 +112,7 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) {
for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) { for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = &now peer.DeactivatedAt = &now
peer.DeactivatedReason = "missing ldap user"
if err := s.UpdatePeer(peer, now); err != nil { if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err) 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) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = nil peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
if err = s.UpdatePeer(peer, now); err != nil { if err = s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err) logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err)
} }

View File

@ -127,6 +127,7 @@ func (s *Server) Setup(ctx context.Context) error {
}) })
s.server.Use(sessions.Sessions("authsession", cookieStore)) s.server.Use(sessions.Sessions("authsession", cookieStore))
s.server.SetFuncMap(template.FuncMap{ s.server.SetFuncMap(template.FuncMap{
"formatDate": common.FormatDateHTML,
"formatBytes": common.ByteCountSI, "formatBytes": common.ByteCountSI,
"urlEncode": url.QueryEscape, "urlEncode": url.QueryEscape,
"startsWith": strings.HasPrefix, "startsWith": strings.HasPrefix,

View File

@ -62,7 +62,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
} }
// CreatePeerByEmail creates a new peer for the given email. // 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) user := s.users.GetUser(email)
peer, err := s.PrepareNewPeer(device) peer, err := s.PrepareNewPeer(device)
@ -75,10 +75,6 @@ func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disab
} else { } else {
peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix) peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix)
} }
now := time.Now()
if disabled {
peer.DeactivatedAt = &now
}
return s.CreatePeer(device, peer) 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) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = nil peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
if err := s.UpdatePeer(peer, now); err != nil { 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) 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) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = &now peer.DeactivatedAt = &now
peer.DeactivatedReason = "user deleted"
if err := s.UpdatePeer(peer, now); err != nil { if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err) logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err)
} }

View File

@ -1,4 +1,4 @@
package server package server
var Version = "testbuild" var Version = "testbuild"
var DatabaseVersion = "1.0.8" var DatabaseVersion = "1.0.9"

View File

@ -108,11 +108,15 @@ type Peer struct {
// Global Device Settings (can be ignored, only make sense if device is in server mode) // Global Device Settings (can be ignored, only make sense if device is in server mode)
Mtu int `form:"mtu" binding:"gte=0,lte=1500"` Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
DeactivatedAt *time.Time `json:",omitempty"` DeactivatedAt *time.Time `json:",omitempty"`
CreatedBy string DeactivatedReason string `json:",omitempty"`
UpdatedBy string
CreatedAt time.Time ExpiresAt *time.Time `json:",omitempty" form:"expires_at" binding:"omitempty" time_format:"2006-01-02"`
UpdatedAt time.Time
CreatedBy string
UpdatedBy string
CreatedAt time.Time
UpdatedAt time.Time
} }
func (p *Peer) SetIPAddresses(addresses ...string) { func (p *Peer) SetIPAddresses(addresses ...string) {
@ -238,6 +242,19 @@ func (p Peer) IsValid() bool {
return true 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 { func (p Peer) GetConfigFileName() string {
reg := regexp.MustCompile("[^a-zA-Z0-9_-]+") reg := regexp.MustCompile("[^a-zA-Z0-9_-]+")
return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf" return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf"