Compare commits

...

7 Commits

Author SHA1 Message Date
Christoph Haas
83271b5d34 fix user edit bug, allow to delete users from the database (#40) 2022-03-15 23:34:55 +01:00
Alexis
cc50fcf8e6 Feat/ldap certificate connexion (#92)
* Give the way to connect against LDAP server with certificate and key

* fix(ldap) Update cert variable name

In order to be more explicit

Co-authored-by: Alexis Aurin <alexis@so6.pw>
2022-03-15 22:46:00 +01:00
Christoph Haas
5d4d06db81 fix invalid interface public key (#74) 2021-12-16 19:51:45 +01:00
ultram4rine
e581b3a69f Wireguard exporter friendly tags (#81)
* add friendly name

* add friendly name as option to configuration

* add friendly name configuration to readme
2021-12-16 19:35:15 +01:00
Alexander Beck
acb629f672 do not overwrite preshared key in CreatePeer (#77) 2021-12-10 16:52:44 +01:00
Christoph Haas
b5cb967e09 improve ldap logging (#67) 2021-11-07 13:20:16 +01:00
commonism
5a9918e00d docker-compose - use logging limits (#66)
- ldap sync is very noisy, limits/rotation required
 - can be verified with
   docker inspect -f '{{.HostConfig.LogConfig}}' 88…de
   {json-file map[max-file:3 max-size:10m]}

Co-authored-by: Markus Koetter <koetter@cispa.de>
2021-11-04 22:52:14 +01:00
17 changed files with 226 additions and 105 deletions

126
README.md
View File

@@ -8,9 +8,9 @@
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal) ![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/) [![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). 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 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 interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
connections. connections.
The configuration portal currently supports using SQLite and MySQL as a user source for authentication and profile data. The configuration portal currently supports using SQLite and MySQL as a user source for authentication and profile data.
@@ -31,11 +31,11 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
* Can be used with existing WireGuard setups * Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces * Support for multiple WireGuard interfaces
* REST API for management and client deployment * REST API for management and client deployment
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
## Setup ## 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. 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 ### Docker
@@ -108,57 +108,61 @@ For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
### Configuration Options ### Configuration Options
The following configuration options are available: The following configuration options are available:
| environment | yaml | yaml_parent | default_value | description | | 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. | | 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. | | 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. | | WEBSITE_TITLE | title | core | WireGuard VPN | The website title. |
| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). | | COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). |
| MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. | | MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. |
| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. | | 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_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. | | 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. | | 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. | | 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. | | SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. | | WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). |
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. | | LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. | | SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
| DATABASE_HOST | host | database | | The mysql server address. | | DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
| DATABASE_PORT | port | database | | The mysql server port. | | DATABASE_HOST | host | database | | The mysql server address. |
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. | | DATABASE_PORT | port | database | | The mysql server port. |
| DATABASE_USERNAME | user | database | | The mysql user. | | DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
| DATABASE_PASSWORD | password | database | | The mysql password. | | DATABASE_USERNAME | user | database | | The mysql user. |
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. | | DATABASE_PASSWORD | password | database | | The mysql password. |
| EMAIL_PORT | port | email | 25 | The email server port. | | EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. | | EMAIL_PORT | port | email | 25 | The email server port. |
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. | | EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. | | EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. | | EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. | | EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. | | EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. | | EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). | | WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. | | WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. | | WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. | | MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. | | LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. | | LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. | | LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. | | LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. | | LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
| 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_PASSWORD | pass | ldap | SuperSecret | The bind password. |
| 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_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_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. | | 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_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. | | LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. | | LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. | | LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. | | LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. | | LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. | | LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
| LOG_JSON | | | false | Format log output as JSON. | | LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
| LOG_COLOR | | | true | Colorize log output. | | LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
| CONFIG_FILE | | | config.yml | The config file 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 ### Sample yaml configuration
config.yml: config.yml:
@@ -189,7 +193,7 @@ email:
user: test@gmail.com user: test@gmail.com
pass: topsecret pass: topsecret
wg: wg:
devices: devices:
- wg0 - wg0
- wg1 - wg1
defaultDevice: wg0 defaultDevice: wg0
@@ -198,8 +202,8 @@ wg:
``` ```
### RESTful API ### RESTful API
WireGuard Portal offers a RESTful API to interact with. WireGuard Portal offers a RESTful API to interact with.
The API is documented using OpenAPI 2.0, the Swagger UI can be found The API is documented using OpenAPI 2.0, the Swagger UI can be found
under the URL `http://<your wg-portal ip/domain>/swagger/index.html?displayOperationId=true`. under the URL `http://<your wg-portal ip/domain>/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. The [API's unittesting](tests/test_API.py) may serve as an example how to make use of the API with python3 & pyswagger.
@@ -209,7 +213,7 @@ The [API's unittesting](tests/test_API.py) may serve as an example how to make u
* Generation or application of any `iptables` or `nftables` rules. * 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. * Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux.
* Importing private keys of an existing WireGuard setup. * Importing private keys of an existing WireGuard setup.
## Application stack ## Application stack
* [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin) * [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin)
@@ -220,6 +224,6 @@ The [API's unittesting](tests/test_API.py) may serve as an example how to make u
## License ## License
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT * 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). This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web).

View File

@@ -76,6 +76,11 @@
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
<a href="/admin/users/" class="btn btn-secondary">Cancel</a> <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> </form>
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}

View File

@@ -4,6 +4,10 @@ services:
image: h44z/wg-portal:1.0.6 image: h44z/wg-portal:1.0.6
container_name: wg-portal container_name: wg-portal
restart: unless-stopped restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
network_mode: "host" network_mode: "host"

View File

@@ -2,6 +2,7 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"io/ioutil"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -154,23 +155,49 @@ func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authent
} }
func (provider Provider) open() (*ldap.Conn, error) { 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)) conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig))
if err != nil { if err != nil {
return nil, err return nil, errors.WithMessage(err, "failed to connect to LDAP")
} }
if provider.config.StartTLS { if provider.config.StartTLS {
// Reconnect with TLS // Reconnect with TLS
err = conn.StartTLS(tlsConfig) err = conn.StartTLS(tlsConfig)
if err != nil { 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) err = conn.Bind(provider.config.BindUser, provider.config.BindPass)
if err != nil { if err != nil {
return nil, err return nil, errors.WithMessage(err, "failed to bind user")
} }
return conn, nil return conn, nil

View File

@@ -4,7 +4,6 @@ import (
gldap "github.com/go-ldap/ldap/v3" gldap "github.com/go-ldap/ldap/v3"
) )
type Type string type Type string
const ( const (
@@ -26,8 +25,11 @@ type Config struct {
PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"` PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"`
GroupMemberAttribute string `yaml:"attrGroups" envconfig:"LDAP_ATTR_GROUPS"` 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 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"` 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 string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal
AdminLdapGroup_ *gldap.DN `yaml:"-"` 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"`
} }

View File

@@ -2,6 +2,7 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"io/ioutil"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -14,7 +15,33 @@ type RawLdapData struct {
} }
func Open(cfg *Config) (*ldap.Conn, error) { 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)) conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to connect to LDAP") return nil, errors.Wrap(err, "failed to connect to LDAP")

View File

@@ -67,6 +67,7 @@ type Config struct {
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"` EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"` CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"` SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"`
WGExoprterFriendlyNames bool `yaml:"wgExporterFriendlyNames" envconfig:"WG_EXPORTER_FRIENDLY_NAMES"`
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"` LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"` SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"` LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"`
@@ -91,6 +92,7 @@ func NewConfig() *Config {
cfg.Core.AdminPassword = "wgportal" cfg.Core.AdminPassword = "wgportal"
cfg.Core.LdapEnabled = false cfg.Core.LdapEnabled = false
cfg.Core.EditableKeys = true cfg.Core.EditableKeys = true
cfg.Core.WGExoprterFriendlyNames = false
cfg.Core.SessionSecret = "secret" cfg.Core.SessionSecret = "secret"
cfg.Database.Typ = "sqlite" cfg.Database.Typ = "sqlite"

View File

@@ -112,7 +112,7 @@ func (s *Server) GetInterfaceConfig(c *gin.Context) {
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
device := s.peers.GetDevice(currentSession.DeviceName) device := s.peers.GetDevice(currentSession.DeviceName)
peers := s.peers.GetActivePeers(device.DeviceName) peers := s.peers.GetActivePeers(device.DeviceName)
cfg, err := device.GetConfigFile(peers) cfg, err := device.GetConfigFile(peers, s.config.Core.WGExoprterFriendlyNames)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return

View File

@@ -236,7 +236,7 @@ func (s *Server) GetPeerConfig(c *gin.Context) {
return return
} }
cfg, err := peer.GetConfigFile(s.peers.GetDevice(currentSession.DeviceName)) cfg, err := peer.GetConfigFile(s.peers.GetDevice(peer.DeviceName))
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return

View File

@@ -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) { func (s *Server) PostAdminUsersEdit(c *gin.Context) {
currentUser := s.users.GetUserUnscoped(c.Query("pkey")) currentUser := s.users.GetUserUnscoped(c.Query("pkey"))
if currentUser == nil { if currentUser == nil {
@@ -113,7 +133,7 @@ func (s *Server) PostAdminUsersEdit(c *gin.Context) {
} else { } else {
formUser.DeletedAt = gorm.DeletedAt{} formUser.DeletedAt = gorm.DeletedAt{}
} }
formUser.IsAdmin = c.PostForm("isadmin") == "true" formUser.IsAdmin = c.PostForm("isadmin") != ""
if err := s.UpdateUser(formUser); err != nil { if err := s.UpdateUser(formUser); err != nil {
_ = s.updateFormInSession(c, formUser) _ = s.updateFormInSession(c, formUser)

View File

@@ -44,14 +44,14 @@ func (s *Server) SyncLdapWithUserDatabase() {
logrus.Info("ldap user synchronization stopped") 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 { if s.config.LDAP.AdminLdapGroup_ == nil {
return false return false
} }
for _, group := range ldapData.RawAttributes[s.config.LDAP.GroupMemberAttribute] { 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) { if s.config.LDAP.AdminLdapGroup_.Equal(dn) {
return true return true
} }
} }
return false return false
@@ -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) logrus.Errorf("failed to delete deactivated user %s in database: %v", activeUsers[i].Email, err)
} }
} }

View File

@@ -64,6 +64,7 @@ func SetupRoutes(s *Server) {
admin.GET("/users/create", s.GetAdminUsersCreate) admin.GET("/users/create", s.GetAdminUsersCreate)
admin.POST("/users/create", s.PostAdminUsersCreate) admin.POST("/users/create", s.PostAdminUsersCreate)
admin.GET("/users/edit", s.GetAdminUsersEdit) admin.GET("/users/edit", s.GetAdminUsersEdit)
admin.GET("/users/delete", s.GetAdminUsersDelete)
admin.POST("/users/edit", s.PostAdminUsersEdit) admin.POST("/users/edit", s.PostAdminUsersEdit)
// User routes // User routes

View File

@@ -103,16 +103,21 @@ func (s *Server) CreatePeer(device string, peer wireguard.Peer) error {
} }
peer.SetIPAddresses(peerIPs...) 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() psk, err := wgtypes.GenerateKey()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate key") 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() key, err := wgtypes.GeneratePrivateKey()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate private key") return errors.Wrap(err, "failed to generate private key")
} }
peer.PresharedKey = psk.String()
peer.PrivateKey = key.String() peer.PrivateKey = key.String()
peer.PublicKey = key.PublicKey().String() peer.PublicKey = key.PublicKey().String()
} }
@@ -204,7 +209,7 @@ func (s *Server) WriteWireGuardConfigFile(device string) error {
} }
dev := s.peers.GetDevice(device) 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 { if err != nil {
return errors.WithMessage(err, "failed to get config file") 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. // 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. // 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 { func (s *Server) UpdateUser(user users.User) error {
if user.DeletedAt.Valid {
return s.DeleteUser(user)
}
currentUser := s.users.GetUserUnscoped(user.Email) currentUser := s.users.GetUserUnscoped(user.Email)
// Hash user password (if set) // 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") 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 { if currentUser.DeletedAt.Valid {
for _, peer := range s.peers.GetPeersByMail(user.Email) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
@@ -284,24 +290,38 @@ func (s *Server) UpdateUser(user users.User) error {
return nil 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. // Also, if the user has linked WireGuard peers, they will be deactivated.
func (s *Server) DeleteUser(user users.User) error { func (s *Server) DeleteUser(user users.User) error {
currentUser := s.users.GetUserUnscoped(user.Email)
// Update in database // 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") return errors.WithMessage(err, "failed to delete user in manager")
} }
// If user was active, disable it's peers // remove all linked peers
if !currentUser.DeletedAt.Valid { for _, peer := range s.peers.GetPeersByMail(user.Email) {
for _, peer := range s.peers.GetPeersByMail(user.Email) { if err := s.DeletePeer(peer); err != nil {
now := time.Now() logrus.Errorf("failed to delete peer %s for %s: %v", peer.PublicKey, user.Email, err)
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)
}
} }
} }

View File

@@ -161,9 +161,14 @@ func (m Manager) UpdateUser(user *User) error {
return nil return nil
} }
func (m Manager) DeleteUser(user *User) error { func (m Manager) DeleteUser(user *User, soft bool) error {
user.Email = strings.ToLower(user.Email) 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 { if res.Error != nil {
return errors.Wrapf(res.Error, "failed to update user %s", user.Email) return errors.Wrapf(res.Error, "failed to update user %s", user.Email)
} }

View File

@@ -29,7 +29,7 @@ type User struct {
// required fields // required fields
Email string `gorm:"primaryKey" form:"email" binding:"required,email"` Email string `gorm:"primaryKey" form:"email" binding:"required,email"`
Source UserSource Source UserSource
IsAdmin bool IsAdmin bool `form:"isadmin"`
// optional fields // optional fields
Firstname string `form:"firstname" binding:"required"` Firstname string `form:"firstname" binding:"required"`

View File

@@ -338,12 +338,13 @@ func (d Device) GetConfig() wgtypes.Config {
return cfg return cfg
} }
func (d Device) GetConfigFile(peers []Peer) ([]byte, error) { func (d Device) GetConfigFile(peers []Peer, friendlyNames bool) ([]byte, error) {
var tplBuff bytes.Buffer var tplBuff bytes.Buffer
err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{ err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{
"Peers": peers, "Peers": peers,
"Interface": d, "Interface": d,
"FriendlyNames": friendlyNames,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to execute server template") return nil, errors.Wrap(err, "failed to execute server template")

View File

@@ -56,6 +56,9 @@ PostDown = {{ .Interface.PostDown }}
# -WGP- PrivateKey: {{.PrivateKey}} # -WGP- PrivateKey: {{.PrivateKey}}
{{- end}} {{- end}}
[Peer] [Peer]
{{- if $.FriendlyNames}}
# friendly_name = {{ .Identifier }}
{{- end}}
PublicKey = {{ .PublicKey }} PublicKey = {{ .PublicKey }}
{{- if .PresharedKey}} {{- if .PresharedKey}}
PresharedKey = {{ .PresharedKey }} PresharedKey = {{ .PresharedKey }}
@@ -75,4 +78,4 @@ Endpoint = {{ .Endpoint }}
PersistentKeepalive = {{ .PersistentKeepalive }} PersistentKeepalive = {{ .PersistentKeepalive }}
{{- end}} {{- end}}
{{- end}} {{- end}}
{{end}} {{end}}