diff --git a/README.md b/README.md index d05d7d1..269cc83 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal) [![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](https://hub.docker.com/r/h44z/wg-portal/) -A simple, web based configuration portal for [WireGuard](https://wireguard.com). -The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN -interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN +A simple, web based configuration portal for [WireGuard](https://wireguard.com). +The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN +interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN connections. The configuration portal currently supports using SQLite and MySQL as a user source for authentication and profile data. @@ -31,11 +31,11 @@ * Can be used with existing WireGuard setups * Support for multiple WireGuard interfaces * REST API for management and client deployment - + ![Screenshot](screenshot.png) ## Setup -Make sure that your host system has at least one WireGuard interface (for example wg0) available. +Make sure that your host system has at least one WireGuard interface (for example wg0) available. If you did not start up a WireGuard interface yet, take a look at [wg-quick](https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html) in order to get started. ### Docker @@ -156,6 +156,9 @@ | 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. | @@ -190,7 +193,7 @@ user: test@gmail.com pass: topsecret wg: - devices: + devices: - wg0 - wg1 defaultDevice: wg0 @@ -199,8 +202,8 @@ ``` ### RESTful API -WireGuard Portal offers a RESTful API to interact with. -The API is documented using OpenAPI 2.0, the Swagger UI can be found +WireGuard Portal offers a RESTful API to interact with. +The API is documented using OpenAPI 2.0, the Swagger UI can be found under the URL `http:///swagger/index.html?displayOperationId=true`. The [API's unittesting](tests/test_API.py) may serve as an example how to make use of the API with python3 & pyswagger. @@ -210,7 +213,7 @@ * Generation or application of any `iptables` or `nftables` rules. * Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux. * Importing private keys of an existing WireGuard setup. - + ## Application stack * [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin) @@ -221,6 +224,6 @@ ## License * MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT - + This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web). diff --git a/assets/tpl/admin_edit_user.html b/assets/tpl/admin_edit_user.html index 9891f32..1218ca5 100644 --- a/assets/tpl/admin_edit_user.html +++ b/assets/tpl/admin_edit_user.html @@ -76,6 +76,11 @@ Cancel + {{if eq $.Session.IsAdmin true}} + {{if eq .User.Source "db"}} + Delete + {{end}} + {{end}} {{template "prt_footer.html" .}} diff --git a/internal/authentication/providers/ldap/provider.go b/internal/authentication/providers/ldap/provider.go index 26f66e0..dff2575 100644 --- a/internal/authentication/providers/ldap/provider.go +++ b/internal/authentication/providers/ldap/provider.go @@ -2,6 +2,7 @@ import ( "crypto/tls" + "io/ioutil" "strings" "github.com/gin-gonic/gin" @@ -154,7 +155,33 @@ } func (provider Provider) open() (*ldap.Conn, error) { - tlsConfig := &tls.Config{InsecureSkipVerify: !provider.config.CertValidation} + var tlsConfig *tls.Config + + if provider.config.LdapCertConn { + + cert_plain, err := ioutil.ReadFile(provider.config.LdapTlsCert) + if err != nil { + return nil, errors.WithMessage(err, "failed to load the certificate") + + } + + key, err := ioutil.ReadFile(provider.config.LdapTlsKey) + if err != nil { + return nil, errors.WithMessage(err, "failed to load the key") + } + + cert_x509, err := tls.X509KeyPair(cert_plain, key) + if err != nil { + return nil, errors.WithMessage(err, "failed X509") + + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert_x509}} + + } else { + + tlsConfig = &tls.Config{InsecureSkipVerify: !provider.config.CertValidation} + } + conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig)) if err != nil { return nil, errors.WithMessage(err, "failed to connect to LDAP") diff --git a/internal/ldap/config.go b/internal/ldap/config.go index ff8a11c..c88bd83 100644 --- a/internal/ldap/config.go +++ b/internal/ldap/config.go @@ -4,7 +4,6 @@ gldap "github.com/go-ldap/ldap/v3" ) - type Type string const ( @@ -26,8 +25,11 @@ PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"` GroupMemberAttribute string `yaml:"attrGroups" envconfig:"LDAP_ATTR_GROUPS"` - LoginFilter string `yaml:"loginFilter" envconfig:"LDAP_LOGIN_FILTER"` // {{login_identifier}} gets replaced with the login email address - SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"` - AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal + LoginFilter string `yaml:"loginFilter" envconfig:"LDAP_LOGIN_FILTER"` // {{login_identifier}} gets replaced with the login email address + SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"` + AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal AdminLdapGroup_ *gldap.DN `yaml:"-"` + LdapCertConn bool `yaml:"ldapCertConn" envconfig:"LDAP_CERT_CONN"` + LdapTlsCert string `yaml:"ldapTlsCert" envconfig:"LDAPTLS_CERT"` + LdapTlsKey string `yaml:"ldapTlsKey" envconfig:"LDAPTLS_KEY"` } diff --git a/internal/ldap/ldap.go b/internal/ldap/ldap.go index 38af07b..d1d2c84 100644 --- a/internal/ldap/ldap.go +++ b/internal/ldap/ldap.go @@ -2,6 +2,7 @@ import ( "crypto/tls" + "io/ioutil" "github.com/go-ldap/ldap/v3" "github.com/pkg/errors" @@ -14,7 +15,33 @@ } func Open(cfg *Config) (*ldap.Conn, error) { - tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.CertValidation} + var tlsConfig *tls.Config + + if cfg.LdapCertConn { + + cert_plain, err := ioutil.ReadFile(cfg.LdapTlsCert) + if err != nil { + return nil, errors.WithMessage(err, "failed to load the certificate") + + } + + key, err := ioutil.ReadFile(cfg.LdapTlsKey) + if err != nil { + return nil, errors.WithMessage(err, "failed to load the key") + } + + cert_x509, err := tls.X509KeyPair(cert_plain, key) + if err != nil { + return nil, errors.WithMessage(err, "failed X509") + + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert_x509}} + + } else { + + tlsConfig = &tls.Config{InsecureSkipVerify: !cfg.CertValidation} + } + conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig)) if err != nil { return nil, errors.Wrap(err, "failed to connect to LDAP") diff --git a/internal/server/handlers_user.go b/internal/server/handlers_user.go index c0816f5..4660105 100644 --- a/internal/server/handlers_user.go +++ b/internal/server/handlers_user.go @@ -83,6 +83,26 @@ }) } +func (s *Server) GetAdminUsersDelete(c *gin.Context) { + user := s.users.GetUserUnscoped(c.Query("pkey")) + if user == nil { + SetFlashMessage(c, "invalid user", "danger") + c.Redirect(http.StatusSeeOther, "/admin/users/") + return + } + + urlEncodedKey := url.QueryEscape(c.Query("pkey")) + + if err := s.HardDeleteUser(*user); err != nil { + SetFlashMessage(c, "failed to delete user: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=delete") + return + } + + SetFlashMessage(c, "user deleted successfully", "success") + c.Redirect(http.StatusSeeOther, "/admin/users/") +} + func (s *Server) PostAdminUsersEdit(c *gin.Context) { currentUser := s.users.GetUserUnscoped(c.Query("pkey")) if currentUser == nil { @@ -113,7 +133,7 @@ } else { formUser.DeletedAt = gorm.DeletedAt{} } - formUser.IsAdmin = c.PostForm("isadmin") == "true" + formUser.IsAdmin = c.PostForm("isadmin") != "" if err := s.UpdateUser(formUser); err != nil { _ = s.updateFormInSession(c, formUser) diff --git a/internal/server/ldapsync.go b/internal/server/ldapsync.go index eb9fc5c..55429d1 100644 --- a/internal/server/ldapsync.go +++ b/internal/server/ldapsync.go @@ -44,14 +44,14 @@ logrus.Info("ldap user synchronization stopped") } -func (s Server)userIsInAdminGroup(ldapData *ldap.RawLdapData) bool { +func (s Server) userIsInAdminGroup(ldapData *ldap.RawLdapData) bool { if s.config.LDAP.AdminLdapGroup_ == nil { - return false + return false } for _, group := range ldapData.RawAttributes[s.config.LDAP.GroupMemberAttribute] { - var dn,_ = gldap.ParseDN(string(group)) + var dn, _ = gldap.ParseDN(string(group)) if s.config.LDAP.AdminLdapGroup_.Equal(dn) { - return true + return true } } return false @@ -114,7 +114,7 @@ } } - if err := s.users.DeleteUser(&activeUsers[i]); err != nil { + if err := s.users.DeleteUser(&activeUsers[i], true); err != nil { logrus.Errorf("failed to delete deactivated user %s in database: %v", activeUsers[i].Email, err) } } diff --git a/internal/server/routes.go b/internal/server/routes.go index 5d10776..3922cad 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -64,6 +64,7 @@ admin.GET("/users/create", s.GetAdminUsersCreate) admin.POST("/users/create", s.PostAdminUsersCreate) admin.GET("/users/edit", s.GetAdminUsersEdit) + admin.GET("/users/delete", s.GetAdminUsersDelete) admin.POST("/users/edit", s.PostAdminUsersEdit) // User routes diff --git a/internal/server/server_helper.go b/internal/server/server_helper.go index 1b433f2..f5d3248 100644 --- a/internal/server/server_helper.go +++ b/internal/server/server_helper.go @@ -113,7 +113,7 @@ peer.PresharedKey = psk.String() } - if peer.PrivateKey == "" && peer.PublicKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one + if peer.PrivateKey == "" && peer.PublicKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one key, err := wgtypes.GeneratePrivateKey() if err != nil { @@ -254,10 +254,6 @@ // UpdateUser updates the user in the database. If the user is marked as deleted, it will get remove from the database. // Also, if the user is re-enabled, all it's linked WireGuard peers will be activated again. func (s *Server) UpdateUser(user users.User) error { - if user.DeletedAt.Valid { - return s.DeleteUser(user) - } - currentUser := s.users.GetUserUnscoped(user.Email) // Hash user password (if set) @@ -276,7 +272,12 @@ return errors.WithMessage(err, "failed to update user in manager") } - // If user was deleted (disabled), reactivate it's peers + // Set to deleted (disabled) if user's deletedAt date is not empty + if user.DeletedAt.Valid { + return s.DeleteUser(user) + } + + // Otherwise, if user was deleted (disabled), reactivate it's peers if currentUser.DeletedAt.Valid { for _, peer := range s.peers.GetPeersByMail(user.Email) { now := time.Now() @@ -290,24 +291,38 @@ return nil } -// DeleteUser removes the user from the database. +// DeleteUser soft-deletes the user from the database (disable the user). // Also, if the user has linked WireGuard peers, they will be deactivated. func (s *Server) DeleteUser(user users.User) error { - currentUser := s.users.GetUserUnscoped(user.Email) - // Update in database - if err := s.users.DeleteUser(&user); err != nil { + if err := s.users.DeleteUser(&user, true); err != nil { + return errors.WithMessage(err, "failed to disable user in manager") + } + + // Disable users peers + for _, peer := range s.peers.GetPeersByMail(user.Email) { + now := time.Now() + peer.DeactivatedAt = &now + if err := s.UpdatePeer(peer, now); err != nil { + logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err) + } + } + + return nil +} + +// HardDeleteUser removes the user from the database. +// Also, if the user has linked WireGuard peers, they will be deleted. +func (s *Server) HardDeleteUser(user users.User) error { + // Update in database + if err := s.users.DeleteUser(&user, false); err != nil { return errors.WithMessage(err, "failed to delete user in manager") } - // If user was active, disable it's peers - if !currentUser.DeletedAt.Valid { - for _, peer := range s.peers.GetPeersByMail(user.Email) { - now := time.Now() - peer.DeactivatedAt = &now - if err := s.UpdatePeer(peer, now); err != nil { - logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err) - } + // remove all linked peers + for _, peer := range s.peers.GetPeersByMail(user.Email) { + if err := s.DeletePeer(peer); err != nil { + logrus.Errorf("failed to delete peer %s for %s: %v", peer.PublicKey, user.Email, err) } } diff --git a/internal/users/manager.go b/internal/users/manager.go index 53fb5d4..5a9c489 100644 --- a/internal/users/manager.go +++ b/internal/users/manager.go @@ -161,9 +161,14 @@ return nil } -func (m Manager) DeleteUser(user *User) error { +func (m Manager) DeleteUser(user *User, soft bool) error { user.Email = strings.ToLower(user.Email) - res := m.db.Delete(user) + var res *gorm.DB + if soft { + res = m.db.Delete(user) + } else { + res = m.db.Unscoped().Delete(user) + } if res.Error != nil { return errors.Wrapf(res.Error, "failed to update user %s", user.Email) } diff --git a/internal/users/user.go b/internal/users/user.go index 24397c7..2a94fcf 100644 --- a/internal/users/user.go +++ b/internal/users/user.go @@ -29,7 +29,7 @@ // required fields Email string `gorm:"primaryKey" form:"email" binding:"required,email"` Source UserSource - IsAdmin bool + IsAdmin bool `form:"isadmin"` // optional fields Firstname string `form:"firstname" binding:"required"`