package server // go get -u github.com/swaggo/swag/cmd/swag // run: swag init --parseDependency --parseInternal --generalInfo api.go // in the internal/server folder import ( "encoding/json" "net/http" "strings" "time" jsonpatch "github.com/evanphx/json-patch" "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/wireguard" ) // @title WireGuard Portal API // @version 1.0 // @description WireGuard Portal API for managing users and peers. // @license.name MIT // @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 // @in header // @name Authorization // @scope.admin Admin access required // @securityDefinitions.basic GeneralBasicAuth // @in header // @name Authorization // @scope.user User access required // @BasePath /api/v1 // ApiServer is a simple wrapper struct so that we can have fresh member function names. type ApiServer struct { s *Server } type ApiError struct { Message string } // GetUsers godoc // @Tags Users // @Summary Retrieves all users // @Produce json // @Success 200 {object} []users.User // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Router /backend/users [get] // @Security ApiBasicAuth func (s *ApiServer) GetUsers(c *gin.Context) { allUsers := s.s.users.GetUsersUnscoped() c.JSON(http.StatusOK, allUsers) } // GetUser godoc // @Tags Users // @Summary Retrieves user based on given Email // @Produce json // @Param email path string true "User Email" // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Router /backend/user/{email} [get] // @Security ApiBasicAuth func (s *ApiServer) GetUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } user := s.s.users.GetUserUnscoped(c.Param("email")) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } c.JSON(http.StatusOK, user) } // PostUser godoc // @Tags Users // @Summary Creates a new user based on the given user model // @Accept json // @Produce json // @Param user body users.User true "User Model" // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /backend/users [post] // @Security ApiBasicAuth func (s *ApiServer) PostUser(c *gin.Context) { newUser := users.User{} if err := c.BindJSON(&newUser); err != nil { c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) return } if user := s.s.users.GetUserUnscoped(newUser.Email); user != nil { c.JSON(http.StatusBadRequest, ApiError{Message: "user already exists"}) return } if err := s.s.CreateUser(newUser, s.s.wg.Cfg.GetDefaultDeviceName()); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } user := s.s.users.GetUserUnscoped(newUser.Email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } c.JSON(http.StatusOK, user) } // PutUser godoc // @Tags Users // @Summary Updates a user based on the given user model // @Accept json // @Produce json // @Param email path string true "User Email" // @Param user body users.User true "User Model" // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /backend/user/{email} [put] // @Security ApiBasicAuth func (s *ApiServer) PutUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } updateUser := users.User{} if err := c.BindJSON(&updateUser); err != nil { c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) return } // Changing email address is not allowed if email != updateUser.Email { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"}) return } if user := s.s.users.GetUserUnscoped(email); user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"}) return } if err := s.s.UpdateUser(updateUser); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } user := s.s.users.GetUserUnscoped(email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } c.JSON(http.StatusOK, user) } // PatchUser godoc // @Tags Users // @Summary Updates a user based on the given partial user model // @Accept json // @Produce json // @Param email path string true "User Email" // @Param user body users.User true "User Model" // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /backend/user/{email} [patch] // @Security ApiBasicAuth func (s *ApiServer) PatchUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } patch, err := c.GetRawData() if err != nil { c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) return } user := s.s.users.GetUserUnscoped(email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"}) return } userData, err := json.Marshal(user) if err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } mergedUserData, err := jsonpatch.MergePatch(userData, patch) var mergedUser users.User err = json.Unmarshal(mergedUserData, &mergedUser) if err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } // CHanging email address is not allowed if email != mergedUser.Email { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"}) return } if err := s.s.UpdateUser(mergedUser); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } user = s.s.users.GetUserUnscoped(email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } c.JSON(http.StatusOK, user) } // DeleteUser godoc // @Tags Users // @Summary Deletes the specified user // @Produce json // @Param email path string true "User Email" // @Success 204 "No content" // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /backend/user/{email} [delete] // @Security ApiBasicAuth func (s *ApiServer) DeleteUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } var user *users.User if user = s.s.users.GetUserUnscoped(email); user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"}) return } if err := s.s.DeleteUser(*user); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } 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) } type PeerDeploymentInformation struct { PublicKey string Identifier string Device string DeviceIdentifier string } // GetPeerDeploymentInformation godoc // @Tags Provisioning // @Summary Retrieves all active peers for the given email address // @Produce json // @Param email path string true "Email Address" // @Success 200 {object} []PeerDeploymentInformation "All active WireGuard peers" // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Router /provisioning/peers/{email} [get] // @Security GeneralBasicAuth func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) { email := c.Param("email") if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } // Get authenticated user to check permissions username, _, _ := c.Request.BasicAuth() user := s.s.users.GetUser(username) if !user.IsAdmin && user.Email != email { c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"}) return } peers := s.s.peers.GetPeersByMail(email) result := make([]PeerDeploymentInformation, 0, len(peers)) for i := range peers { if peers[i].DeactivatedAt != nil { continue // skip deactivated peers } device := s.s.peers.GetDevice(peers[i].DeviceName) if device.Type != wireguard.DeviceTypeServer { continue // Skip peers on non-server devices } result = append(result, PeerDeploymentInformation{ PublicKey: peers[i].PublicKey, Identifier: peers[i].Identifier, Device: device.DeviceName, DeviceIdentifier: device.DisplayName, }) } c.JSON(http.StatusOK, result) } // 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/peers [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) }