mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-06 16:36:18 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
83271b5d34 | ||
|
cc50fcf8e6 | ||
|
5d4d06db81 | ||
|
e581b3a69f | ||
|
acb629f672 | ||
|
b5cb967e09 | ||
|
5a9918e00d |
@@ -109,7 +109,7 @@ For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
|
||||
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. |
|
||||
@@ -121,6 +121,7 @@ The following configuration options are available:
|
||||
| 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. |
|
||||
@@ -155,6 +156,9 @@ The following configuration options are available:
|
||||
| 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. |
|
||||
|
@@ -76,6 +76,11 @@
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
{{if eq .User.Source "db"}}
|
||||
<a href="/admin/users/delete?pkey={{.User.Email}}" data-toggle="confirmation" data-title="Really delete user and associated peers?" title="Delete user and associated peers" class="btn btn-danger float-right">Delete</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
|
@@ -4,6 +4,10 @@ services:
|
||||
image: h44z/wg-portal:1.0.6
|
||||
container_name: wg-portal
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
network_mode: "host"
|
||||
|
@@ -2,6 +2,7 @@ package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -154,23 +155,49 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
|
||||
}
|
||||
|
||||
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, err
|
||||
return nil, errors.WithMessage(err, "failed to connect to LDAP")
|
||||
}
|
||||
|
||||
if provider.config.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.WithMessage(err, "failed to start TLS session")
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Bind(provider.config.BindUser, provider.config.BindPass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.WithMessage(err, "failed to bind user")
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
|
@@ -4,7 +4,6 @@ import (
|
||||
gldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
@@ -30,4 +29,7 @@ type Config struct {
|
||||
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"`
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/pkg/errors"
|
||||
@@ -14,7 +15,33 @@ type RawLdapData struct {
|
||||
}
|
||||
|
||||
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")
|
||||
|
@@ -67,6 +67,7 @@ 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"`
|
||||
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
|
||||
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
|
||||
LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"`
|
||||
@@ -91,6 +92,7 @@ func NewConfig() *Config {
|
||||
cfg.Core.AdminPassword = "wgportal"
|
||||
cfg.Core.LdapEnabled = false
|
||||
cfg.Core.EditableKeys = true
|
||||
cfg.Core.WGExoprterFriendlyNames = false
|
||||
cfg.Core.SessionSecret = "secret"
|
||||
|
||||
cfg.Database.Typ = "sqlite"
|
||||
|
@@ -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)
|
||||
cfg, err := device.GetConfigFile(peers, s.config.Core.WGExoprterFriendlyNames)
|
||||
if err != nil {
|
||||
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
|
||||
return
|
||||
|
@@ -236,7 +236,7 @@ func (s *Server) GetPeerConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := peer.GetConfigFile(s.peers.GetDevice(currentSession.DeviceName))
|
||||
cfg, err := peer.GetConfigFile(s.peers.GetDevice(peer.DeviceName))
|
||||
if err != nil {
|
||||
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
|
||||
return
|
||||
|
@@ -83,6 +83,26 @@ func (s *Server) GetAdminUsersEdit(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
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 @@ func (s *Server) PostAdminUsersEdit(c *gin.Context) {
|
||||
} 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)
|
||||
|
@@ -44,12 +44,12 @@ func (s *Server) SyncLdapWithUserDatabase() {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -64,6 +64,7 @@ func SetupRoutes(s *Server) {
|
||||
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
|
||||
|
@@ -103,16 +103,21 @@ func (s *Server) CreatePeer(device string, peer wireguard.Peer) error {
|
||||
}
|
||||
peer.SetIPAddresses(peerIPs...)
|
||||
}
|
||||
if peer.PrivateKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one
|
||||
if peer.PresharedKey == "" && dev.Type == wireguard.DeviceTypeServer { // if preshared key is empty create a new one
|
||||
|
||||
psk, err := wgtypes.GenerateKey()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate key")
|
||||
}
|
||||
peer.PresharedKey = psk.String()
|
||||
}
|
||||
|
||||
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 {
|
||||
return errors.Wrap(err, "failed to generate private key")
|
||||
}
|
||||
peer.PresharedKey = psk.String()
|
||||
peer.PrivateKey = key.String()
|
||||
peer.PublicKey = key.PublicKey().String()
|
||||
}
|
||||
@@ -204,7 +209,7 @@ func (s *Server) WriteWireGuardConfigFile(device string) error {
|
||||
}
|
||||
|
||||
dev := s.peers.GetDevice(device)
|
||||
cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device))
|
||||
cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device), s.config.Core.WGExoprterFriendlyNames)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get config file")
|
||||
}
|
||||
@@ -248,10 +253,6 @@ func (s *Server) CreateUser(user users.User, device string) error {
|
||||
// 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)
|
||||
@@ -270,7 +271,12 @@ func (s *Server) UpdateUser(user users.User) error {
|
||||
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()
|
||||
@@ -284,18 +290,15 @@ func (s *Server) UpdateUser(user users.User) error {
|
||||
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 {
|
||||
return errors.WithMessage(err, "failed to delete user in manager")
|
||||
if err := s.users.DeleteUser(&user, true); err != nil {
|
||||
return errors.WithMessage(err, "failed to disable user in manager")
|
||||
}
|
||||
|
||||
// If user was active, disable it's peers
|
||||
if !currentUser.DeletedAt.Valid {
|
||||
// Disable users peers
|
||||
for _, peer := range s.peers.GetPeersByMail(user.Email) {
|
||||
now := time.Now()
|
||||
peer.DeactivatedAt = &now
|
||||
@@ -303,6 +306,23 @@ func (s *Server) DeleteUser(user users.User) error {
|
||||
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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@@ -161,9 +161,14 @@ func (m Manager) UpdateUser(user *User) error {
|
||||
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)
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ type User struct {
|
||||
// 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"`
|
||||
|
@@ -338,12 +338,13 @@ func (d Device) GetConfig() wgtypes.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (d Device) GetConfigFile(peers []Peer) ([]byte, error) {
|
||||
func (d Device) GetConfigFile(peers []Peer, friendlyNames bool) ([]byte, error) {
|
||||
var tplBuff bytes.Buffer
|
||||
|
||||
err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{
|
||||
"Peers": peers,
|
||||
"Interface": d,
|
||||
"FriendlyNames": friendlyNames,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute server template")
|
||||
|
@@ -56,6 +56,9 @@ PostDown = {{ .Interface.PostDown }}
|
||||
# -WGP- PrivateKey: {{.PrivateKey}}
|
||||
{{- end}}
|
||||
[Peer]
|
||||
{{- if $.FriendlyNames}}
|
||||
# friendly_name = {{ .Identifier }}
|
||||
{{- end}}
|
||||
PublicKey = {{ .PublicKey }}
|
||||
{{- if .PresharedKey}}
|
||||
PresharedKey = {{ .PresharedKey }}
|
||||
|
Reference in New Issue
Block a user