mirror of
https://github.com/h44z/wg-portal.git
synced 2025-04-19 08:55:12 +00:00
RESTful API for WireGuard Portal (#11)
This commit is contained in:
parent
35513ae994
commit
87964f8ec4
@ -114,6 +114,7 @@ The following configuration options are available:
|
|||||||
| 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. |
|
||||||
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
|
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
|
||||||
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
|
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
|
||||||
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
|
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
|
||||||
@ -191,6 +192,11 @@ wg:
|
|||||||
manageIPAddresses: true
|
manageIPAddresses: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
under the URL `http://<your wg-portal ip/domain>/swagger/index.html`.
|
||||||
|
|
||||||
## What is out of scope
|
## What is out of scope
|
||||||
|
|
||||||
* Generation or application of any `iptables` or `nftables` rules
|
* Generation or application of any `iptables` or `nftables` rules
|
||||||
|
@ -136,7 +136,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
admin.Email = email
|
admin.Email = email
|
||||||
admin.Password = string(hashedPassword)
|
admin.Password = users.PrivateString(hashedPassword)
|
||||||
admin.Firstname = "WireGuard"
|
admin.Firstname = "WireGuard"
|
||||||
admin.Lastname = "Administrator"
|
admin.Lastname = "Administrator"
|
||||||
admin.CreatedAt = time.Now()
|
admin.CreatedAt = time.Now()
|
||||||
@ -170,7 +170,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
|
|||||||
return errors.Wrap(err, "failed to hash admin password")
|
return errors.Wrap(err, "failed to hash admin password")
|
||||||
}
|
}
|
||||||
|
|
||||||
admin.Password = string(hashedPassword)
|
admin.Password = users.PrivateString(hashedPassword)
|
||||||
admin.IsAdmin = true
|
admin.IsAdmin = true
|
||||||
admin.UpdatedAt = time.Now()
|
admin.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
@ -7,10 +7,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
jsonpatch "github.com/evanphx/json-patch"
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/common"
|
||||||
"github.com/h44z/wg-portal/internal/users"
|
"github.com/h44z/wg-portal/internal/users"
|
||||||
|
"github.com/h44z/wg-portal/internal/wireguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title WireGuard Portal API
|
// @title WireGuard Portal API
|
||||||
@ -20,9 +23,18 @@ import (
|
|||||||
// @license.name MIT
|
// @license.name MIT
|
||||||
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
|
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
// @contact.name WireGuard Portal Project
|
||||||
|
// @contact.url https://github.com/h44z/wg-portal
|
||||||
|
|
||||||
// @securityDefinitions.basic ApiBasicAuth
|
// @securityDefinitions.basic ApiBasicAuth
|
||||||
// @in header
|
// @in header
|
||||||
// @name Authorization
|
// @name Authorization
|
||||||
|
// @scope.admin Admin access required
|
||||||
|
|
||||||
|
// @securityDefinitions.basic GeneralBasicAuth
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @scope.user User access required
|
||||||
|
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
|
|
||||||
@ -36,24 +48,23 @@ type ApiError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUsers godoc
|
// GetUsers godoc
|
||||||
|
// @Tags Users
|
||||||
// @Summary Retrieves all users
|
// @Summary Retrieves all users
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []users.User
|
// @Success 200 {object} []users.User
|
||||||
// @Failure 401 {object} ApiError
|
// @Failure 401 {object} ApiError
|
||||||
// @Failure 403 {object} ApiError
|
// @Failure 403 {object} ApiError
|
||||||
// @Failure 404 {object} ApiError
|
// @Failure 404 {object} ApiError
|
||||||
// @Router /users [get]
|
// @Router /backend/users [get]
|
||||||
// @Security ApiBasicAuth
|
// @Security ApiBasicAuth
|
||||||
func (s *ApiServer) GetUsers(c *gin.Context) {
|
func (s *ApiServer) GetUsers(c *gin.Context) {
|
||||||
allUsers := s.s.users.GetUsersUnscoped()
|
allUsers := s.s.users.GetUsersUnscoped()
|
||||||
for i := range allUsers {
|
|
||||||
allUsers[i].Password = "" // do not publish password...
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, allUsers)
|
c.JSON(http.StatusOK, allUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser godoc
|
// GetUser godoc
|
||||||
|
// @Tags Users
|
||||||
// @Summary Retrieves user based on given Email
|
// @Summary Retrieves user based on given Email
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param email path string true "User Email"
|
// @Param email path string true "User Email"
|
||||||
@ -62,7 +73,7 @@ func (s *ApiServer) GetUsers(c *gin.Context) {
|
|||||||
// @Failure 401 {object} ApiError
|
// @Failure 401 {object} ApiError
|
||||||
// @Failure 403 {object} ApiError
|
// @Failure 403 {object} ApiError
|
||||||
// @Failure 404 {object} ApiError
|
// @Failure 404 {object} ApiError
|
||||||
// @Router /user/{email} [get]
|
// @Router /backend/user/{email} [get]
|
||||||
// @Security ApiBasicAuth
|
// @Security ApiBasicAuth
|
||||||
func (s *ApiServer) GetUser(c *gin.Context) {
|
func (s *ApiServer) GetUser(c *gin.Context) {
|
||||||
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
||||||
@ -76,20 +87,22 @@ func (s *ApiServer) GetUser(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Password = "" // do not send password...
|
|
||||||
c.JSON(http.StatusOK, user)
|
c.JSON(http.StatusOK, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostUser godoc
|
// PostUser godoc
|
||||||
|
// @Tags Users
|
||||||
// @Summary Creates a new user based on the given user model
|
// @Summary Creates a new user based on the given user model
|
||||||
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
// @Param user body users.User true "User Model"
|
||||||
// @Success 200 {object} users.User
|
// @Success 200 {object} users.User
|
||||||
// @Failure 400 {object} ApiError
|
// @Failure 400 {object} ApiError
|
||||||
// @Failure 401 {object} ApiError
|
// @Failure 401 {object} ApiError
|
||||||
// @Failure 403 {object} ApiError
|
// @Failure 403 {object} ApiError
|
||||||
// @Failure 404 {object} ApiError
|
// @Failure 404 {object} ApiError
|
||||||
// @Failure 500 {object} ApiError
|
// @Failure 500 {object} ApiError
|
||||||
// @Router /users [post]
|
// @Router /backend/users [post]
|
||||||
// @Security ApiBasicAuth
|
// @Security ApiBasicAuth
|
||||||
func (s *ApiServer) PostUser(c *gin.Context) {
|
func (s *ApiServer) PostUser(c *gin.Context) {
|
||||||
newUser := users.User{}
|
newUser := users.User{}
|
||||||
@ -113,21 +126,23 @@ func (s *ApiServer) PostUser(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Password = "" // do not send password...
|
|
||||||
c.JSON(http.StatusOK, user)
|
c.JSON(http.StatusOK, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutUser godoc
|
// PutUser godoc
|
||||||
|
// @Tags Users
|
||||||
// @Summary Updates a user based on the given user model
|
// @Summary Updates a user based on the given user model
|
||||||
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param email path string true "User Email"
|
// @Param email path string true "User Email"
|
||||||
|
// @Param user body users.User true "User Model"
|
||||||
// @Success 200 {object} users.User
|
// @Success 200 {object} users.User
|
||||||
// @Failure 400 {object} ApiError
|
// @Failure 400 {object} ApiError
|
||||||
// @Failure 401 {object} ApiError
|
// @Failure 401 {object} ApiError
|
||||||
// @Failure 403 {object} ApiError
|
// @Failure 403 {object} ApiError
|
||||||
// @Failure 404 {object} ApiError
|
// @Failure 404 {object} ApiError
|
||||||
// @Failure 500 {object} ApiError
|
// @Failure 500 {object} ApiError
|
||||||
// @Router /user/{email} [put]
|
// @Router /backend/user/{email} [put]
|
||||||
// @Security ApiBasicAuth
|
// @Security ApiBasicAuth
|
||||||
func (s *ApiServer) PutUser(c *gin.Context) {
|
func (s *ApiServer) PutUser(c *gin.Context) {
|
||||||
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
||||||
@ -163,21 +178,23 @@ func (s *ApiServer) PutUser(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Password = "" // do not send password...
|
|
||||||
c.JSON(http.StatusOK, user)
|
c.JSON(http.StatusOK, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchUser godoc
|
// PatchUser godoc
|
||||||
|
// @Tags Users
|
||||||
// @Summary Updates a user based on the given partial user model
|
// @Summary Updates a user based on the given partial user model
|
||||||
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param email path string true "User Email"
|
// @Param email path string true "User Email"
|
||||||
|
// @Param user body users.User true "User Model"
|
||||||
// @Success 200 {object} users.User
|
// @Success 200 {object} users.User
|
||||||
// @Failure 400 {object} ApiError
|
// @Failure 400 {object} ApiError
|
||||||
// @Failure 401 {object} ApiError
|
// @Failure 401 {object} ApiError
|
||||||
// @Failure 403 {object} ApiError
|
// @Failure 403 {object} ApiError
|
||||||
// @Failure 404 {object} ApiError
|
// @Failure 404 {object} ApiError
|
||||||
// @Failure 500 {object} ApiError
|
// @Failure 500 {object} ApiError
|
||||||
// @Router /user/{email} [patch]
|
// @Router /backend/user/{email} [patch]
|
||||||
// @Security ApiBasicAuth
|
// @Security ApiBasicAuth
|
||||||
func (s *ApiServer) PatchUser(c *gin.Context) {
|
func (s *ApiServer) PatchUser(c *gin.Context) {
|
||||||
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
||||||
@ -227,11 +244,11 @@ func (s *ApiServer) PatchUser(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Password = "" // do not send password...
|
|
||||||
c.JSON(http.StatusOK, user)
|
c.JSON(http.StatusOK, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser godoc
|
// DeleteUser godoc
|
||||||
|
// @Tags Users
|
||||||
// @Summary Deletes the specified user
|
// @Summary Deletes the specified user
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param email path string true "User Email"
|
// @Param email path string true "User Email"
|
||||||
@ -241,7 +258,7 @@ func (s *ApiServer) PatchUser(c *gin.Context) {
|
|||||||
// @Failure 403 {object} ApiError
|
// @Failure 403 {object} ApiError
|
||||||
// @Failure 404 {object} ApiError
|
// @Failure 404 {object} ApiError
|
||||||
// @Failure 500 {object} ApiError
|
// @Failure 500 {object} ApiError
|
||||||
// @Router /user/{email} [delete]
|
// @Router /backend/user/{email} [delete]
|
||||||
// @Security ApiBasicAuth
|
// @Security ApiBasicAuth
|
||||||
func (s *ApiServer) DeleteUser(c *gin.Context) {
|
func (s *ApiServer) DeleteUser(c *gin.Context) {
|
||||||
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
email := strings.ToLower(strings.TrimSpace(c.Param("email")))
|
||||||
@ -263,3 +280,590 @@ func (s *ApiServer) DeleteUser(c *gin.Context) {
|
|||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeers godoc
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Retrieves all peers for the given interface
|
||||||
|
// @Produce json
|
||||||
|
// @Param device path string true "Device Name"
|
||||||
|
// @Success 200 {object} []wireguard.Peer
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Router /backend/peers/{device} [get]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) GetPeers(c *gin.Context) {
|
||||||
|
deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
|
||||||
|
if deviceName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate device name
|
||||||
|
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := s.s.peers.GetAllPeers(deviceName)
|
||||||
|
c.JSON(http.StatusOK, peers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeer godoc
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Retrieves the peer for the given public key
|
||||||
|
// @Produce json
|
||||||
|
// @Param pkey path string true "Public Key (Base 64)"
|
||||||
|
// @Success 200 {object} wireguard.Peer
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Router /backend/peer/{pkey} [get]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) GetPeer(c *gin.Context) {
|
||||||
|
pkey := c.Param("pkey")
|
||||||
|
if pkey == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := s.s.peers.GetPeerByKey(pkey)
|
||||||
|
if !peer.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostPeer godoc
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Creates a new peer based on the given peer model
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param device path string true "Device Name"
|
||||||
|
// @Param peer body wireguard.Peer true "Peer Model"
|
||||||
|
// @Success 200 {object} wireguard.Peer
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Failure 500 {object} ApiError
|
||||||
|
// @Router /backend/peers/{device} [post]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) PostPeer(c *gin.Context) {
|
||||||
|
deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
|
||||||
|
if deviceName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate device name
|
||||||
|
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPeer := wireguard.Peer{}
|
||||||
|
if err := c.BindJSON(&newPeer); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer := s.s.peers.GetPeerByKey(newPeer.PublicKey); peer.IsValid() {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "peer already exists"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.s.CreatePeer(deviceName, newPeer); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := s.s.peers.GetPeerByKey(newPeer.PublicKey)
|
||||||
|
if !peer.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutPeer godoc
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Updates the given peer based on the given peer model
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param pkey path string true "Public Key"
|
||||||
|
// @Param peer body wireguard.Peer true "Peer Model"
|
||||||
|
// @Success 200 {object} wireguard.Peer
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Failure 500 {object} ApiError
|
||||||
|
// @Router /backend/peer/{pkey} [put]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) PutPeer(c *gin.Context) {
|
||||||
|
updatePeer := wireguard.Peer{}
|
||||||
|
if err := c.BindJSON(&updatePeer); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pkey := c.Param("pkey")
|
||||||
|
if pkey == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer := s.s.peers.GetPeerByKey(pkey); !peer.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing public key is not allowed
|
||||||
|
if pkey != updatePeer.PublicKey {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if updatePeer.DeactivatedAt != nil {
|
||||||
|
updatePeer.DeactivatedAt = &now
|
||||||
|
}
|
||||||
|
if err := s.s.UpdatePeer(updatePeer, now); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := s.s.peers.GetPeerByKey(updatePeer.PublicKey)
|
||||||
|
if !peer.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchPeer godoc
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Updates the given peer based on the given partial peer model
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param pkey path string true "Public Key"
|
||||||
|
// @Param peer body wireguard.Peer true "Peer Model"
|
||||||
|
// @Success 200 {object} wireguard.Peer
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Failure 500 {object} ApiError
|
||||||
|
// @Router /backend/peer/{pkey} [patch]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) PatchPeer(c *gin.Context) {
|
||||||
|
patch, err := c.GetRawData()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pkey := c.Param("pkey")
|
||||||
|
if pkey == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := s.s.peers.GetPeerByKey(pkey)
|
||||||
|
if !peer.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerData, err := json.Marshal(peer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedPeerData, err := jsonpatch.MergePatch(peerData, patch)
|
||||||
|
var mergedPeer wireguard.Peer
|
||||||
|
err = json.Unmarshal(mergedPeerData, &mergedPeer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mergedPeer.IsValid() {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "invalid peer model"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing public key is not allowed
|
||||||
|
if pkey != mergedPeer.PublicKey {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if mergedPeer.DeactivatedAt != nil {
|
||||||
|
mergedPeer.DeactivatedAt = &now
|
||||||
|
}
|
||||||
|
if err := s.s.UpdatePeer(mergedPeer, now); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer = s.s.peers.GetPeerByKey(mergedPeer.PublicKey)
|
||||||
|
if !peer.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePeer godoc
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Updates the given peer based on the given partial peer model
|
||||||
|
// @Produce json
|
||||||
|
// @Param pkey path string true "Public Key"
|
||||||
|
// @Success 202 "No Content"
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Failure 500 {object} ApiError
|
||||||
|
// @Router /backend/peer/{pkey} [delete]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) DeletePeer(c *gin.Context) {
|
||||||
|
pkey := c.Param("pkey")
|
||||||
|
if pkey == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := s.s.peers.GetPeerByKey(pkey)
|
||||||
|
if peer.PublicKey == "" {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.s.DeletePeer(peer); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDevices godoc
|
||||||
|
// @Tags Interface
|
||||||
|
// @Summary Get all devices
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []wireguard.Device
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Router /backend/devices [get]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) GetDevices(c *gin.Context) {
|
||||||
|
var devices []wireguard.Device
|
||||||
|
for _, deviceName := range s.s.config.WG.DeviceNames {
|
||||||
|
device := s.s.peers.GetDevice(deviceName)
|
||||||
|
if !device.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
devices = append(devices, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDevice godoc
|
||||||
|
// @Tags Interface
|
||||||
|
// @Summary Get the given device
|
||||||
|
// @Produce json
|
||||||
|
// @Param device path string true "Device Name"
|
||||||
|
// @Success 200 {object} wireguard.Device
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Router /backend/device/{device} [get]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) GetDevice(c *gin.Context) {
|
||||||
|
deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
|
||||||
|
if deviceName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate device name
|
||||||
|
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
device := s.s.peers.GetDevice(deviceName)
|
||||||
|
if !device.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "device not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutDevice godoc
|
||||||
|
// @Tags Interface
|
||||||
|
// @Summary Updates the given device based on the given device model (UNIMPLEMENTED)
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param device path string true "Device Name"
|
||||||
|
// @Param body body wireguard.Device true "Device Model"
|
||||||
|
// @Success 200 {object} wireguard.Device
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Failure 500 {object} ApiError
|
||||||
|
// @Router /backend/device/{device} [put]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) PutDevice(c *gin.Context) {
|
||||||
|
updateDevice := wireguard.Device{}
|
||||||
|
if err := c.BindJSON(&updateDevice); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
|
||||||
|
if deviceName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate device name
|
||||||
|
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
device := s.s.peers.GetDevice(deviceName)
|
||||||
|
if !device.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing device name is not allowed
|
||||||
|
if deviceName != updateDevice.DeviceName {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
|
||||||
|
c.JSON(http.StatusNotImplemented, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchDevice godoc
|
||||||
|
// @Tags Interface
|
||||||
|
// @Summary Updates the given device based on the given partial device model (UNIMPLEMENTED)
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param device path string true "Device Name"
|
||||||
|
// @Param body body wireguard.Device true "Device Model"
|
||||||
|
// @Success 200 {object} wireguard.Device
|
||||||
|
// @Failure 400 {object} ApiError
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Failure 500 {object} ApiError
|
||||||
|
// @Router /backend/device/{device} [patch]
|
||||||
|
// @Security ApiBasicAuth
|
||||||
|
func (s *ApiServer) PatchDevice(c *gin.Context) {
|
||||||
|
patch, err := c.GetRawData()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
|
||||||
|
if deviceName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate device name
|
||||||
|
if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
device := s.s.peers.GetDevice(deviceName)
|
||||||
|
if !device.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceData, err := json.Marshal(device)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedDeviceData, err := jsonpatch.MergePatch(deviceData, patch)
|
||||||
|
var mergedDevice wireguard.Device
|
||||||
|
err = json.Unmarshal(mergedDeviceData, &mergedDevice)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mergedDevice.IsValid() {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "invalid device model"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing device name is not allowed
|
||||||
|
if deviceName != mergedDevice.DeviceName {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
|
||||||
|
c.JSON(http.StatusNotImplemented, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeerDeploymentConfig godoc
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Retrieves the peer config for the given public key
|
||||||
|
// @Produce plain
|
||||||
|
// @Param pkey path string true "Public Key (Base 64)"
|
||||||
|
// @Success 200 {object} string "The WireGuard configuration file"
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Router /provisioning/peer/{pkey} [get]
|
||||||
|
// @Security GeneralBasicAuth
|
||||||
|
func (s *ApiServer) GetPeerDeploymentConfig(c *gin.Context) {
|
||||||
|
pkey := c.Param("pkey")
|
||||||
|
if pkey == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := s.s.peers.GetPeerByKey(pkey)
|
||||||
|
if !peer.IsValid() {
|
||||||
|
c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get authenticated user to check permissions
|
||||||
|
username, _, _ := c.Request.BasicAuth()
|
||||||
|
user := s.s.users.GetUser(username)
|
||||||
|
|
||||||
|
if !user.IsAdmin && user.Email == peer.Email {
|
||||||
|
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
device := s.s.peers.GetDevice(peer.DeviceName)
|
||||||
|
config, err := peer.GetConfigFile(device)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "text/plain", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningRequest struct {
|
||||||
|
// DeviceName is optional, if not specified, the configured default device will be used.
|
||||||
|
DeviceName string `json:",omitempty"`
|
||||||
|
Identifier string `binding:"required"`
|
||||||
|
Email string `binding:"required"`
|
||||||
|
|
||||||
|
// Client specific and optional settings
|
||||||
|
|
||||||
|
AllowedIPsStr string `binding:"cidrlist" json:",omitempty"`
|
||||||
|
PersistentKeepalive int `binding:"gte=0" json:",omitempty"`
|
||||||
|
DNSStr string `binding:"iplist" json:",omitempty"`
|
||||||
|
Mtu int `binding:"gte=0,lte=1500" json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostPeerDeploymentConfig godoc
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Creates the requested peer config and returns the config file
|
||||||
|
// @Accept json
|
||||||
|
// @Produce plain
|
||||||
|
// @Param body body ProvisioningRequest true "Provisioning Request Model"
|
||||||
|
// @Success 200 {object} string "The WireGuard configuration file"
|
||||||
|
// @Failure 401 {object} ApiError
|
||||||
|
// @Failure 403 {object} ApiError
|
||||||
|
// @Failure 404 {object} ApiError
|
||||||
|
// @Router /provisioning/peer [post]
|
||||||
|
// @Security GeneralBasicAuth
|
||||||
|
func (s *ApiServer) PostPeerDeploymentConfig(c *gin.Context) {
|
||||||
|
req := ProvisioningRequest{}
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get authenticated user to check permissions
|
||||||
|
username, _, _ := c.Request.BasicAuth()
|
||||||
|
user := s.s.users.GetUser(username)
|
||||||
|
|
||||||
|
if !user.IsAdmin && !s.s.config.Core.SelfProvisioningAllowed {
|
||||||
|
c.JSON(http.StatusForbidden, ApiError{Message: "peer provisioning service disabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.IsAdmin && user.Email == req.Email {
|
||||||
|
c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceName := req.DeviceName
|
||||||
|
if deviceName == "" || !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
|
||||||
|
deviceName = s.s.config.WG.GetDefaultDeviceName()
|
||||||
|
}
|
||||||
|
device := s.s.peers.GetDevice(deviceName)
|
||||||
|
if device.Type != wireguard.DeviceTypeServer {
|
||||||
|
c.JSON(http.StatusForbidden, ApiError{Message: "invalid device, provisioning disabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if private/public keys are set, if so check database for existing entries
|
||||||
|
peer, err := s.s.PrepareNewPeer(deviceName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer.Email = req.Email
|
||||||
|
peer.Identifier = req.Identifier
|
||||||
|
|
||||||
|
if req.AllowedIPsStr != "" {
|
||||||
|
peer.AllowedIPsStr = req.AllowedIPsStr
|
||||||
|
}
|
||||||
|
if req.PersistentKeepalive != 0 {
|
||||||
|
peer.PersistentKeepalive = req.PersistentKeepalive
|
||||||
|
}
|
||||||
|
if req.DNSStr != "" {
|
||||||
|
peer.DNSStr = req.DNSStr
|
||||||
|
}
|
||||||
|
if req.Mtu != 0 {
|
||||||
|
peer.Mtu = req.Mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.s.CreatePeer(deviceName, peer); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := peer.GetConfigFile(device)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "text/plain", config)
|
||||||
|
}
|
||||||
|
@ -55,17 +55,18 @@ func loadConfigEnv(cfg interface{}) error {
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Core struct {
|
Core struct {
|
||||||
ListeningAddress string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"`
|
ListeningAddress string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"`
|
||||||
ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
|
ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
|
||||||
Title string `yaml:"title" envconfig:"WEBSITE_TITLE"`
|
Title string `yaml:"title" envconfig:"WEBSITE_TITLE"`
|
||||||
CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"`
|
CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"`
|
||||||
MailFrom string `yaml:"mailFrom" envconfig:"MAIL_FROM"`
|
MailFrom string `yaml:"mailFrom" envconfig:"MAIL_FROM"`
|
||||||
AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"` // must be an email address
|
AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"` // must be an email address
|
||||||
AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
|
AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
|
||||||
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"`
|
||||||
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
|
SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"`
|
||||||
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
|
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
|
||||||
|
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
|
||||||
} `yaml:"core"`
|
} `yaml:"core"`
|
||||||
Database common.DatabaseConfig `yaml:"database"`
|
Database common.DatabaseConfig `yaml:"database"`
|
||||||
Email common.MailConfig `yaml:"email"`
|
Email common.MailConfig `yaml:"email"`
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/h44z/wg-portal/internal/users"
|
"github.com/h44z/wg-portal/internal/users"
|
||||||
csrf "github.com/utrack/gin-csrf"
|
csrf "github.com/utrack/gin-csrf"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,19 +104,6 @@ func (s *Server) PostAdminUsersEdit(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if formUser.Password != "" {
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
_ = s.updateFormInSession(c, formUser)
|
|
||||||
SetFlashMessage(c, "failed to hash admin password", "danger")
|
|
||||||
c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=bind")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
formUser.Password = string(hashedPassword)
|
|
||||||
} else {
|
|
||||||
formUser.Password = currentUser.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
disabled := c.PostForm("isdisabled") != ""
|
disabled := c.PostForm("isdisabled") != ""
|
||||||
if disabled {
|
if disabled {
|
||||||
formUser.DeletedAt = gorm.DeletedAt{
|
formUser.DeletedAt = gorm.DeletedAt{
|
||||||
@ -175,15 +161,7 @@ func (s *Server) PostAdminUsersCreate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if formUser.Password != "" {
|
if formUser.Password == "" {
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
SetFlashMessage(c, "failed to hash admin password", "danger")
|
|
||||||
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=bind")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
formUser.Password = string(hashedPassword)
|
|
||||||
} else {
|
|
||||||
_ = s.updateFormInSession(c, formUser)
|
_ = s.updateFormInSession(c, formUser)
|
||||||
SetFlashMessage(c, "invalid password", "danger")
|
SetFlashMessage(c, "invalid password", "danger")
|
||||||
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")
|
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")
|
||||||
|
@ -10,9 +10,18 @@ import (
|
|||||||
_ "github.com/h44z/wg-portal/internal/server/docs" // docs is generated by Swag CLI, you have to import it.
|
_ "github.com/h44z/wg-portal/internal/server/docs" // docs is generated by Swag CLI, you have to import it.
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
"github.com/swaggo/gin-swagger/swaggerFiles"
|
"github.com/swaggo/gin-swagger/swaggerFiles"
|
||||||
|
csrf "github.com/utrack/gin-csrf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRoutes(s *Server) {
|
func SetupRoutes(s *Server) {
|
||||||
|
csrfMiddleware := csrf.Middleware(csrf.Options{
|
||||||
|
Secret: s.config.Core.SessionSecret,
|
||||||
|
ErrorFunc: func(c *gin.Context) {
|
||||||
|
c.String(400, "CSRF token mismatch")
|
||||||
|
c.Abort()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Startpage
|
// Startpage
|
||||||
s.server.GET("/", s.GetIndex)
|
s.server.GET("/", s.GetIndex)
|
||||||
s.server.GET("/favicon.ico", func(c *gin.Context) {
|
s.server.GET("/favicon.ico", func(c *gin.Context) {
|
||||||
@ -26,12 +35,14 @@ func SetupRoutes(s *Server) {
|
|||||||
|
|
||||||
// Auth routes
|
// Auth routes
|
||||||
auth := s.server.Group("/auth")
|
auth := s.server.Group("/auth")
|
||||||
|
auth.Use(csrfMiddleware)
|
||||||
auth.GET("/login", s.GetLogin)
|
auth.GET("/login", s.GetLogin)
|
||||||
auth.POST("/login", s.PostLogin)
|
auth.POST("/login", s.PostLogin)
|
||||||
auth.GET("/logout", s.GetLogout)
|
auth.GET("/logout", s.GetLogout)
|
||||||
|
|
||||||
// Admin routes
|
// Admin routes
|
||||||
admin := s.server.Group("/admin")
|
admin := s.server.Group("/admin")
|
||||||
|
admin.Use(csrfMiddleware)
|
||||||
admin.Use(s.RequireAuthentication("admin"))
|
admin.Use(s.RequireAuthentication("admin"))
|
||||||
admin.GET("/", s.GetAdminIndex)
|
admin.GET("/", s.GetAdminIndex)
|
||||||
admin.GET("/device/edit", s.GetAdminEditInterface)
|
admin.GET("/device/edit", s.GetAdminEditInterface)
|
||||||
@ -57,6 +68,7 @@ func SetupRoutes(s *Server) {
|
|||||||
|
|
||||||
// User routes
|
// User routes
|
||||||
user := s.server.Group("/user")
|
user := s.server.Group("/user")
|
||||||
|
user.Use(csrfMiddleware)
|
||||||
user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
|
user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
|
||||||
user.GET("/qrcode", s.GetPeerQRCode)
|
user.GET("/qrcode", s.GetPeerQRCode)
|
||||||
user.GET("/profile", s.GetUserIndex)
|
user.GET("/profile", s.GetUserIndex)
|
||||||
@ -68,15 +80,35 @@ func SetupRoutes(s *Server) {
|
|||||||
func SetupApiRoutes(s *Server) {
|
func SetupApiRoutes(s *Server) {
|
||||||
api := ApiServer{s: s}
|
api := ApiServer{s: s}
|
||||||
|
|
||||||
// Auth routes
|
// Admin authenticated routes
|
||||||
apiV1 := s.server.Group("/api/v1")
|
apiV1Backend := s.server.Group("/api/v1/backend")
|
||||||
apiV1.Use(s.RequireApiAuthentication("admin"))
|
apiV1Backend.Use(s.RequireApiAuthentication("admin"))
|
||||||
apiV1.GET("/users", api.GetUsers)
|
|
||||||
apiV1.POST("/users", api.PostUser)
|
apiV1Backend.GET("/users", api.GetUsers)
|
||||||
apiV1.GET("/user/:email", api.GetUser)
|
apiV1Backend.POST("/users", api.PostUser)
|
||||||
apiV1.PUT("/user/:email", api.PutUser)
|
apiV1Backend.GET("/user/:email", api.GetUser)
|
||||||
apiV1.PATCH("/user/:email", api.PatchUser)
|
apiV1Backend.PUT("/user/:email", api.PutUser)
|
||||||
apiV1.DELETE("/user/:email", api.DeleteUser)
|
apiV1Backend.PATCH("/user/:email", api.PatchUser)
|
||||||
|
apiV1Backend.DELETE("/user/:email", api.DeleteUser)
|
||||||
|
|
||||||
|
apiV1Backend.GET("/peers/:device", api.GetPeers)
|
||||||
|
apiV1Backend.POST("/peers/:device", api.PostPeer)
|
||||||
|
apiV1Backend.GET("/peer/:pkey", api.GetPeer)
|
||||||
|
apiV1Backend.PUT("/peer/:pkey", api.PutPeer)
|
||||||
|
apiV1Backend.PATCH("/peer/:pkey", api.PatchPeer)
|
||||||
|
apiV1Backend.DELETE("/peer/:pkey", api.DeletePeer)
|
||||||
|
|
||||||
|
apiV1Backend.GET("/devices", api.GetDevices)
|
||||||
|
apiV1Backend.GET("/device/:device", api.GetDevice)
|
||||||
|
apiV1Backend.PUT("/device/:device", api.PutDevice)
|
||||||
|
apiV1Backend.PATCH("/device/:device", api.PatchDevice)
|
||||||
|
|
||||||
|
// Simple authenticated routes
|
||||||
|
apiV1Deployment := s.server.Group("/api/v1/provisioning")
|
||||||
|
apiV1Deployment.Use(s.RequireApiAuthentication(""))
|
||||||
|
|
||||||
|
apiV1Deployment.GET("/peer/:pkey", api.GetPeerDeploymentConfig)
|
||||||
|
apiV1Deployment.POST("/peer", api.PostPeerDeploymentConfig)
|
||||||
|
|
||||||
// Swagger doc/ui
|
// Swagger doc/ui
|
||||||
s.server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
s.server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
@ -26,7 +26,6 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
ginlogrus "github.com/toorop/gin-logrus"
|
ginlogrus "github.com/toorop/gin-logrus"
|
||||||
csrf "github.com/utrack/gin-csrf"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -118,13 +117,6 @@ func (s *Server) Setup(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
s.server.Use(gin.Recovery())
|
s.server.Use(gin.Recovery())
|
||||||
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte(s.config.Core.SessionSecret))))
|
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte(s.config.Core.SessionSecret))))
|
||||||
s.server.Use(csrf.Middleware(csrf.Options{
|
|
||||||
Secret: s.config.Core.SessionSecret,
|
|
||||||
ErrorFunc: func(c *gin.Context) {
|
|
||||||
c.String(400, "CSRF token mismatch")
|
|
||||||
c.Abort()
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
s.server.SetFuncMap(template.FuncMap{
|
s.server.SetFuncMap(template.FuncMap{
|
||||||
"formatBytes": common.ByteCountSI,
|
"formatBytes": common.ByteCountSI,
|
||||||
"urlEncode": url.QueryEscape,
|
"urlEncode": url.QueryEscape,
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/wireguard"
|
"github.com/h44z/wg-portal/internal/wireguard"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -52,6 +53,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
|
|||||||
peer.PersistentKeepalive = dev.DefaultPersistentKeepalive
|
peer.PersistentKeepalive = dev.DefaultPersistentKeepalive
|
||||||
peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
|
peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
|
||||||
peer.Mtu = dev.Mtu
|
peer.Mtu = dev.Mtu
|
||||||
|
peer.DeviceName = device
|
||||||
case wireguard.DeviceTypeClient:
|
case wireguard.DeviceTypeClient:
|
||||||
peer.UID = "newendpoint"
|
peer.UID = "newendpoint"
|
||||||
}
|
}
|
||||||
@ -225,6 +227,15 @@ func (s *Server) CreateUser(user users.User, device string) error {
|
|||||||
return s.UpdateUser(user)
|
return s.UpdateUser(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash user password (if set)
|
||||||
|
if user.Password != "" {
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unable to hash password")
|
||||||
|
}
|
||||||
|
user.Password = users.PrivateString(hashedPassword)
|
||||||
|
}
|
||||||
|
|
||||||
// Create user in database
|
// Create user in database
|
||||||
if err := s.users.CreateUser(&user); err != nil {
|
if err := s.users.CreateUser(&user); err != nil {
|
||||||
return errors.WithMessage(err, "failed to create user in manager")
|
return errors.WithMessage(err, "failed to create user in manager")
|
||||||
@ -243,6 +254,17 @@ func (s *Server) UpdateUser(user users.User) error {
|
|||||||
|
|
||||||
currentUser := s.users.GetUserUnscoped(user.Email)
|
currentUser := s.users.GetUserUnscoped(user.Email)
|
||||||
|
|
||||||
|
// Hash user password (if set)
|
||||||
|
if user.Password != "" {
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unable to hash password")
|
||||||
|
}
|
||||||
|
user.Password = users.PrivateString(hashedPassword)
|
||||||
|
} else {
|
||||||
|
user.Password = currentUser.Password // keep current password
|
||||||
|
}
|
||||||
|
|
||||||
// Update in database
|
// Update in database
|
||||||
if err := s.users.UpdateUser(&user); err != nil {
|
if err := s.users.UpdateUser(&user); err != nil {
|
||||||
return errors.WithMessage(err, "failed to update user in manager")
|
return errors.WithMessage(err, "failed to update user in manager")
|
||||||
|
@ -142,6 +142,7 @@ func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) {
|
|||||||
|
|
||||||
func (m Manager) CreateUser(user *User) error {
|
func (m Manager) CreateUser(user *User) error {
|
||||||
user.Email = strings.ToLower(user.Email)
|
user.Email = strings.ToLower(user.Email)
|
||||||
|
user.Source = UserSourceDatabase
|
||||||
res := m.db.Create(user)
|
res := m.db.Create(user)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
return errors.Wrapf(res.Error, "failed to create user %s", user.Email)
|
return errors.Wrapf(res.Error, "failed to create user %s", user.Email)
|
||||||
|
@ -14,6 +14,16 @@ const (
|
|||||||
UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement
|
UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PrivateString string
|
||||||
|
|
||||||
|
func (PrivateString) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(`""`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PrivateString) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// User is the user model that gets linked to peer entries, by default an empty usermodel with only the email address is created
|
// User is the user model that gets linked to peer entries, by default an empty usermodel with only the email address is created
|
||||||
type User struct {
|
type User struct {
|
||||||
// required fields
|
// required fields
|
||||||
@ -27,10 +37,10 @@ type User struct {
|
|||||||
Phone string `form:"phone" binding:"omitempty"`
|
Phone string `form:"phone" binding:"omitempty"`
|
||||||
|
|
||||||
// optional, integrated password authentication
|
// optional, integrated password authentication
|
||||||
Password string `form:"password" binding:"omitempty"`
|
Password PrivateString `form:"password" binding:"omitempty"`
|
||||||
|
|
||||||
// database internal fields
|
// database internal fields
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -63,21 +63,21 @@ func init() {
|
|||||||
//
|
//
|
||||||
|
|
||||||
type Peer struct {
|
type Peer struct {
|
||||||
Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer
|
Peer *wgtypes.Peer `gorm:"-" json:"-"` // WireGuard peer
|
||||||
Device *Device `gorm:"foreignKey:DeviceName" binding:"-"` // linked WireGuard device
|
Device *Device `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard device
|
||||||
Config string `gorm:"-"`
|
Config string `gorm:"-" json:"-"`
|
||||||
|
|
||||||
UID string `form:"uid" binding:"required,alphanum"` // uid for html identification
|
UID string `form:"uid" binding:"required,alphanum" json:"-"` // uid for html identification
|
||||||
DeviceName string `gorm:"index" form:"device" binding:"required"`
|
DeviceName string `gorm:"index" form:"device" binding:"required"`
|
||||||
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server"`
|
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server" json:"-"`
|
||||||
Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
|
Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
|
||||||
Email string `gorm:"index" form:"mail" binding:"required,email"`
|
Email string `gorm:"index" form:"mail" binding:"required,email"`
|
||||||
IgnoreGlobalSettings bool `form:"ignoreglobalsettings"`
|
IgnoreGlobalSettings bool `form:"ignoreglobalsettings"`
|
||||||
|
|
||||||
IsOnline bool `gorm:"-"`
|
IsOnline bool `gorm:"-" json:"-"`
|
||||||
IsNew bool `gorm:"-"`
|
IsNew bool `gorm:"-" json:"-"`
|
||||||
LastHandshake string `gorm:"-"`
|
LastHandshake string `gorm:"-" json:"-"`
|
||||||
LastHandshakeTime string `gorm:"-"`
|
LastHandshakeTime string `gorm:"-" json:"-"`
|
||||||
|
|
||||||
// Core WireGuard Settings
|
// Core WireGuard Settings
|
||||||
PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself
|
PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself
|
||||||
@ -93,7 +93,7 @@ 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
|
DeactivatedAt *time.Time `json:",omitempty"`
|
||||||
CreatedBy string
|
CreatedBy string
|
||||||
UpdatedBy string
|
UpdatedBy string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
@ -226,7 +226,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
Interface *wgtypes.Device `gorm:"-"`
|
Interface *wgtypes.Device `gorm:"-" json:"-"`
|
||||||
|
|
||||||
Type DeviceType `form:"devicetype" binding:"required,oneof=client server"`
|
Type DeviceType `form:"devicetype" binding:"required,oneof=client server"`
|
||||||
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
|
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user