mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
1333 lines
43 KiB
Go
1333 lines
43 KiB
Go
package controller
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/go-playground/validator/v10"
|
||
"github.com/gorilla/mux"
|
||
"github.com/gravitl/netmaker/db"
|
||
"github.com/gravitl/netmaker/logger"
|
||
"github.com/gravitl/netmaker/logic"
|
||
"github.com/gravitl/netmaker/models"
|
||
"github.com/gravitl/netmaker/orchestrator"
|
||
"github.com/gravitl/netmaker/schema"
|
||
|
||
"github.com/gravitl/netmaker/middleware"
|
||
"github.com/gravitl/netmaker/mq"
|
||
"github.com/gravitl/netmaker/scope"
|
||
"github.com/skip2/go-qrcode"
|
||
"golang.org/x/exp/slices"
|
||
"golang.org/x/exp/slog"
|
||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
var extUpdateMutex = &sync.Mutex{}
|
||
|
||
func extClientHandlers(r *mux.Router) {
|
||
|
||
r.HandleFunc("/api/extclients", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getAllExtClients)))).
|
||
Methods(http.MethodGet)
|
||
r.HandleFunc("/api/extclients/{network}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getNetworkExtClients)))).
|
||
Methods(http.MethodGet)
|
||
r.HandleFunc("/api/extclients/{network}/{clientid}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false, http.HandlerFunc(getExtClient)))).
|
||
Methods(http.MethodGet)
|
||
r.HandleFunc("/api/extclients/{network}/{clientid}/{type}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false, http.HandlerFunc(getExtClientConf)))).
|
||
Methods(http.MethodGet)
|
||
r.HandleFunc("/api/extclients/{network}/{clientid}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false, http.HandlerFunc(updateExtClient)))).
|
||
Methods(http.MethodPut)
|
||
r.HandleFunc("/api/extclients/{network}/{clientid}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false, http.HandlerFunc(deleteExtClient)))).
|
||
Methods(http.MethodDelete)
|
||
r.HandleFunc("/api/v1/extclients/{network}/bulk", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(bulkDeleteExtClients)))).
|
||
Methods(http.MethodDelete)
|
||
r.HandleFunc("/api/v1/extclients/{network}/bulk/status", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(bulkUpdateExtClientStatus)))).
|
||
Methods(http.MethodPut)
|
||
r.HandleFunc("/api/extclients/{network}/{nodeid}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false, http.HandlerFunc(createExtClient)))).
|
||
Methods(http.MethodPost)
|
||
// unused API
|
||
//r.HandleFunc("/api/v1/client_conf/{network}", logic.SecurityCheck(false, http.HandlerFunc(getExtClientHAConf))).Methods(http.MethodGet)
|
||
}
|
||
|
||
func checkIngressExists(nodeID string) bool {
|
||
node, err := logic.GetNodeByID(nodeID)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return node.IsIngressGateway
|
||
}
|
||
|
||
// @Summary Get all config files associated with network
|
||
// @Router /api/extclients/{network} [get]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Success 200 {array} models.ExtClient
|
||
// @Failure 500 {object} models.ErrorResponse
|
||
func getNetworkExtClients(w http.ResponseWriter, r *http.Request) {
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
var extclients []models.ExtClient
|
||
var params = mux.Vars(r)
|
||
network := params["network"]
|
||
extclients, err := logic.GetNetworkExtClients(r.Context(), network)
|
||
if err != nil {
|
||
logger.Log(0, r.Header.Get("user"),
|
||
fmt.Sprintf("failed to get ext clients for network [%s]: %v", network, err))
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
|
||
username := r.Header.Get("user")
|
||
if r.Header.Get("ismaster") != "yes" {
|
||
user := &schema.User{
|
||
Username: username,
|
||
}
|
||
err := user.GetWithMembership(r.Context())
|
||
if err == nil {
|
||
if user.PlatformRoleID != schema.Auditor {
|
||
userRole := &schema.UserRole{
|
||
ID: user.PlatformRoleID,
|
||
}
|
||
err := userRole.GetPlatformRole(r.Context())
|
||
if err != nil || !userRole.TenantGlobalAccess {
|
||
if (user.PlatformRoleID == schema.PlatformUser && !logic.IsNetworkAdmin(r.Context(), user, network)) ||
|
||
user.PlatformRoleID != schema.PlatformUser {
|
||
var filtered []models.ExtClient
|
||
for _, ec := range extclients {
|
||
if logic.IsUserAllowedAccessToExtClient(username, ec) {
|
||
filtered = append(filtered, ec)
|
||
}
|
||
}
|
||
extclients = filtered
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for i := range extclients {
|
||
extclients[i].PrivateKey = ""
|
||
}
|
||
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(extclients)
|
||
}
|
||
|
||
// @Summary Fetch all config files across all networks
|
||
// @Router /api/extclients [get]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Produce json
|
||
// @Success 200 {array} models.ExtClient
|
||
// @Failure 500 {object} models.ErrorResponse
|
||
func getAllExtClients(w http.ResponseWriter, r *http.Request) {
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
clients, err := logic.GetAllExtClients(r.Context())
|
||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
logger.Log(0, "failed to get all extclients: ", err.Error())
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
for i := range clients {
|
||
clients[i].PrivateKey = ""
|
||
}
|
||
logic.SortExtClient(clients[:])
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(clients)
|
||
}
|
||
|
||
// @Summary Get an individual config file
|
||
// @Router /api/extclients/{network}/{clientid} [get]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Param clientid path string true "Client ID"
|
||
// @Success 200 {object} models.ExtClient
|
||
// @Failure 500 {object} models.ErrorResponse
|
||
// @Failure 403 {object} models.ErrorResponse
|
||
func getExtClient(w http.ResponseWriter, r *http.Request) {
|
||
// set header.
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
var params = mux.Vars(r)
|
||
|
||
clientid := params["clientid"]
|
||
network := params["network"]
|
||
client, err := logic.GetExtClient(r.Context(), clientid, network)
|
||
if err != nil {
|
||
logger.Log(
|
||
0,
|
||
r.Header.Get("user"),
|
||
fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
|
||
clientid, network, err),
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
gwNode, err := logic.GetNodeByID(client.IngressGatewayID)
|
||
if err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
logic.SetDNSOnWgConfig(&gwNode, &client)
|
||
client.AllowedIPs = logic.GetExtclientAllowedIPs(r.Context(), client)
|
||
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(client)
|
||
}
|
||
|
||
// @Summary Get config file configuration
|
||
// @Router /api/extclients/{network}/{clientid}/{type} [get]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Param clientid path string true "Client ID"
|
||
// @Param type path string true "Config type (qr or file)"
|
||
// @Param preferredip query string false "Preferred endpoint IP"
|
||
// @Success 200 {object} models.ExtClient
|
||
// @Failure 500 {object} models.ErrorResponse
|
||
// @Failure 403 {object} models.ErrorResponse
|
||
func getExtClientConf(w http.ResponseWriter, r *http.Request) {
|
||
// set header.
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
var params = mux.Vars(r)
|
||
clientid := params["clientid"]
|
||
networkid := params["network"]
|
||
client, err := logic.GetExtClient(r.Context(), clientid, networkid)
|
||
if err != nil {
|
||
logger.Log(
|
||
0,
|
||
r.Header.Get("user"),
|
||
fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
|
||
clientid, networkid, err),
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
|
||
username := r.Header.Get("user")
|
||
if r.Header.Get("ismaster") != "yes" {
|
||
user := &schema.User{
|
||
Username: username,
|
||
}
|
||
err := user.GetWithMembership(r.Context())
|
||
if err == nil {
|
||
if user.PlatformRoleID != schema.SuperAdminRole &&
|
||
user.PlatformRoleID != schema.AdminRole &&
|
||
!(user.PlatformRoleID == schema.PlatformUser && logic.IsNetworkAdmin(r.Context(), user, networkid)) &&
|
||
user.Username != client.OwnerID {
|
||
err = fmt.Errorf("access denied")
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Forbidden))
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
gwnode, err := logic.GetNodeByID(client.IngressGatewayID)
|
||
if err != nil {
|
||
logger.Log(
|
||
0,
|
||
r.Header.Get("user"),
|
||
fmt.Sprintf(
|
||
"failed to get ingress gateway node [%s] info: %v",
|
||
client.IngressGatewayID,
|
||
err,
|
||
),
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
eli, _ := (&schema.Egress{Network: gwnode.Network}).ListByNetwork(r.Context())
|
||
acls := logic.ListNetworkAccessAcls(r.Context(), schema.NetworkID(client.Network))
|
||
logic.GetNodeEgressInfo(&gwnode, eli, acls)
|
||
host := &schema.Host{
|
||
ID: gwnode.HostID,
|
||
}
|
||
err = host.Get(r.Context())
|
||
if err != nil {
|
||
logger.Log(
|
||
0,
|
||
r.Header.Get("user"),
|
||
fmt.Sprintf(
|
||
"failed to get host for ingress gateway node [%s] info: %v",
|
||
client.IngressGatewayID,
|
||
err,
|
||
),
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
|
||
network := &schema.Network{Name: client.Network}
|
||
err = network.Get(r.Context())
|
||
if err != nil {
|
||
logger.Log(
|
||
1,
|
||
r.Header.Get("user"),
|
||
"Could not retrieve Ingress Gateway Network",
|
||
client.Network,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
|
||
preferredIp := strings.TrimSpace(r.URL.Query().Get("preferredip"))
|
||
if preferredIp != "" {
|
||
allowedPreferredIps := []string{}
|
||
for i := range gwnode.AdditionalRagIps {
|
||
allowedPreferredIps = append(allowedPreferredIps, gwnode.AdditionalRagIps[i].String())
|
||
}
|
||
allowedPreferredIps = append(allowedPreferredIps, host.EndpointIP.String())
|
||
allowedPreferredIps = append(allowedPreferredIps, host.EndpointIPv6.String())
|
||
if !slices.Contains(allowedPreferredIps, preferredIp) {
|
||
slog.Warn(
|
||
"preferred endpoint ip is not associated with the RAG. proceeding with preferred ip",
|
||
"preferred ip",
|
||
preferredIp,
|
||
)
|
||
logic.ReturnErrorResponse(
|
||
w,
|
||
r,
|
||
logic.FormatError(
|
||
errors.New("preferred endpoint ip is not associated with the RAG"),
|
||
"badrequest",
|
||
),
|
||
)
|
||
return
|
||
}
|
||
if net.ParseIP(preferredIp).To4() == nil {
|
||
preferredIp = fmt.Sprintf("[%s]", preferredIp)
|
||
}
|
||
}
|
||
|
||
addrString := client.Address
|
||
if addrString != "" {
|
||
addrString += "/32"
|
||
}
|
||
if client.Address6 != "" {
|
||
if addrString != "" {
|
||
addrString += ","
|
||
}
|
||
addrString += client.Address6 + "/128"
|
||
}
|
||
|
||
keepalive := ""
|
||
if host.PersistentKeepalive != 0 {
|
||
keepalive = fmt.Sprintf("PersistentKeepalive = %d", int(host.PersistentKeepalive.Seconds()))
|
||
}
|
||
|
||
gwendpoint := ""
|
||
if preferredIp == "" {
|
||
if host.EndpointIP.To4() == nil {
|
||
gwendpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), host.ListenPort)
|
||
} else {
|
||
gwendpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), host.ListenPort)
|
||
}
|
||
} else {
|
||
gwendpoint = fmt.Sprintf("%s:%d", preferredIp, host.ListenPort)
|
||
}
|
||
|
||
var newAllowedIPs string
|
||
if logic.ExtClientUsesInternetEgress(client, gwnode) {
|
||
egressrange := "0.0.0.0/0"
|
||
if gwnode.Address6.IP != nil && client.Address6 != "" {
|
||
egressrange += "," + "::/0"
|
||
}
|
||
newAllowedIPs = egressrange
|
||
} else {
|
||
newAllowedIPs = network.AddressRange
|
||
if newAllowedIPs != "" && network.AddressRange6 != "" {
|
||
newAllowedIPs += ","
|
||
}
|
||
if network.AddressRange6 != "" {
|
||
newAllowedIPs += network.AddressRange6
|
||
}
|
||
if egressGatewayRanges, err := logic.GetEgressRangesOnNetwork(r.Context(), &client); err == nil {
|
||
for _, egressGatewayRange := range egressGatewayRanges {
|
||
newAllowedIPs += "," + egressGatewayRange
|
||
}
|
||
}
|
||
}
|
||
logic.SetDNSOnWgConfig(&gwnode, &client)
|
||
defaultDNS := ""
|
||
if client.DNS != "" {
|
||
defaultDNS = "DNS = " + client.DNS
|
||
}
|
||
|
||
defaultMTU := 1420
|
||
if host.MTU != 0 {
|
||
defaultMTU = host.MTU
|
||
}
|
||
|
||
postUp := strings.Builder{}
|
||
if client.PostUp != "" && params["type"] != "qr" {
|
||
for _, loc := range strings.Split(client.PostUp, "\n") {
|
||
postUp.WriteString(fmt.Sprintf("PostUp = %s\n", loc))
|
||
}
|
||
}
|
||
|
||
postDown := strings.Builder{}
|
||
if client.PostDown != "" && params["type"] != "qr" {
|
||
for _, loc := range strings.Split(client.PostDown, "\n") {
|
||
postDown.WriteString(fmt.Sprintf("PostDown = %s\n", loc))
|
||
}
|
||
}
|
||
|
||
config := fmt.Sprintf(`[Interface]
|
||
Address = %s
|
||
PrivateKey = %s
|
||
MTU = %d
|
||
%s
|
||
%s
|
||
%s
|
||
|
||
[Peer]
|
||
PublicKey = %s
|
||
AllowedIPs = %s
|
||
Endpoint = %s
|
||
%s
|
||
|
||
`, addrString,
|
||
client.PrivateKey,
|
||
defaultMTU,
|
||
defaultDNS,
|
||
postUp.String(),
|
||
postDown.String(),
|
||
host.PublicKey,
|
||
newAllowedIPs,
|
||
gwendpoint,
|
||
keepalive,
|
||
)
|
||
|
||
if params["type"] == "qr" {
|
||
bytes, err := qrcode.Encode(config, qrcode.Low, -5)
|
||
if err != nil {
|
||
logger.Log(1, r.Header.Get("user"), "failed to encode qr code: ", err.Error())
|
||
if strings.Contains(err.Error(), "content too long to encode") {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(
|
||
fmt.Errorf("config is too large to encode as a QR code; please use the file download instead"),
|
||
"badrequest",
|
||
))
|
||
} else {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
}
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "image/png")
|
||
w.WriteHeader(http.StatusOK)
|
||
_, err = w.Write(bytes)
|
||
if err != nil {
|
||
logger.Log(1, r.Header.Get("user"), "response writer error (qr) ", err.Error())
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
if params["type"] == "file" {
|
||
name := client.ClientID + ".conf"
|
||
w.Header().Set("Content-Type", "application/config")
|
||
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
|
||
w.WriteHeader(http.StatusOK)
|
||
_, err := fmt.Fprint(w, config)
|
||
if err != nil {
|
||
logger.Log(1, r.Header.Get("user"), "response writer error (file) ", err.Error())
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
}
|
||
return
|
||
}
|
||
logger.Log(2, r.Header.Get("user"), "retrieved ext client config")
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(client)
|
||
}
|
||
|
||
// @Summary Create a config file
|
||
// @Router /api/extclients/{network}/{nodeid} [post]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Param nodeid path string true "Node ID (Ingress Gateway)"
|
||
// @Param body body models.CustomExtClient true "Custom ext client parameters"
|
||
// @Success 200 {object} models.ExtClient
|
||
// @Failure 500 {object} models.ErrorResponse
|
||
// @Failure 400 {object} models.ErrorResponse
|
||
// @Failure 403 {object} models.ErrorResponse
|
||
func createExtClient(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
var params = mux.Vars(r)
|
||
nodeid := params["nodeid"]
|
||
ingressExists := checkIngressExists(nodeid)
|
||
if !ingressExists {
|
||
err := errors.New("ingress does not exist")
|
||
slog.Error("failed to create extclient", "user", r.Header.Get("user"), "error", err)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
var customExtClient models.CustomExtClient
|
||
if err := json.NewDecoder(r.Body).Decode(&customExtClient); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
if err := validateCustomExtClient(r.Context(), &customExtClient, true); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
|
||
if logic.ClientLimitExceeded(r.Context()) {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(logic.ErrClientLimitExceeded, logic.Forbidden))
|
||
return
|
||
}
|
||
|
||
var gateway models.EgressGatewayRequest
|
||
gateway.NetID = params["network"]
|
||
gateway.Ranges = customExtClient.ExtraAllowedIPs
|
||
err := logic.ValidateEgressRange(r.Context(), gateway.NetID, gateway.Ranges)
|
||
if err != nil {
|
||
logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
|
||
node, err := logic.GetNodeByID(nodeid)
|
||
if err != nil {
|
||
logger.Log(0, r.Header.Get("user"),
|
||
fmt.Sprintf("failed to get ingress gateway node [%s] info: %v", nodeid, err))
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
var userName string
|
||
var caller *schema.User
|
||
isMaster := r.Header.Get("ismaster") == "yes"
|
||
if isMaster {
|
||
userName = logic.MasterUser
|
||
} else {
|
||
caller = &schema.User{Username: r.Header.Get("user")}
|
||
err = caller.Get(r.Context())
|
||
if err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
userName = caller.Username
|
||
// check if user has a config already for remote access client
|
||
extclients, err := logic.GetNetworkExtClients(r.Context(), node.Network)
|
||
if err != nil {
|
||
slog.Error("failed to get extclients", "error", err)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
var jitGrant *schema.JITGrant
|
||
if extClientCreateRequiresJIT(r.Context(), isMaster, caller, gateway.NetID, customExtClient) {
|
||
hasAccess, grant, err := logic.CheckJITAccess(r.Context(), gateway.NetID, userName)
|
||
if err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
if !hasAccess {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(
|
||
errors.New("JIT access required: please request access from network admin"),
|
||
"forbidden"))
|
||
return
|
||
}
|
||
jitGrant = grant
|
||
}
|
||
// if device id is sent, we don't want to create another extclient for the same user
|
||
// and gw, with the same device id.
|
||
if customExtClient.DeviceID != "" {
|
||
// let's first confirm that none of the user's extclients for this gw have device id.
|
||
for _, extclient := range extclients {
|
||
if extclient.DeviceID == customExtClient.DeviceID &&
|
||
extclient.OwnerID == caller.Username && nodeid == extclient.IngressGatewayID {
|
||
if jitGrant != nil {
|
||
extclient.JITExpiresAt = &jitGrant.ExpiresAt
|
||
_ = logic.SaveExtClient(r.Context(), &extclient)
|
||
}
|
||
err = errors.New("remote client config already exists on the gateway")
|
||
slog.Error("failed to create extclient", "user", userName, "error", err)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
for _, extclient := range extclients {
|
||
if extclient.RemoteAccessClientID != "" &&
|
||
extclient.RemoteAccessClientID == customExtClient.RemoteAccessClientID &&
|
||
extclient.OwnerID == caller.Username && nodeid == extclient.IngressGatewayID {
|
||
if customExtClient.DeviceID != "" && extclient.DeviceID == "" {
|
||
// This extclient doesn’t include a device ID (and neither do the others).
|
||
// We patch it by assigning the device ID from the incoming request.
|
||
// When clients see that the config already exists, they will fetch
|
||
// the one with their device ID. And we will return this one.
|
||
if jitGrant != nil {
|
||
extclient.JITExpiresAt = &jitGrant.ExpiresAt
|
||
}
|
||
extclient.DeviceID = customExtClient.DeviceID
|
||
_ = logic.SaveExtClient(r.Context(), &extclient)
|
||
}
|
||
err = errors.New("remote client config already exists on the gateway")
|
||
slog.Error("failed to create extclient", "user", userName, "error", err)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
extclient := logic.UpdateExtClient(&models.ExtClient{}, &customExtClient)
|
||
|
||
extclient.OwnerID = userName
|
||
extclient.RemoteAccessClientID = customExtClient.RemoteAccessClientID
|
||
extclient.IngressGatewayID = nodeid
|
||
extclient.Network = node.Network
|
||
extclient.Tags = make(map[models.TagID]struct{})
|
||
// extclient.Tags[models.TagID(fmt.Sprintf("%s.%s", extclient.Network,
|
||
// models.RemoteAccessTagName))] = struct{}{}
|
||
// set extclient dns to ingressdns if extclient dns is not explicitly
|
||
gwDNS := logic.GetGwDNS(&node)
|
||
if (extclient.DNS == "") && (gwDNS != "") {
|
||
dns := gwDNS
|
||
extclient.DNS = dns
|
||
}
|
||
host := &schema.Host{
|
||
ID: node.HostID,
|
||
}
|
||
err = host.Get(r.Context())
|
||
if err != nil {
|
||
logger.Log(0, r.Header.Get("user"),
|
||
fmt.Sprintf("failed to get ingress gateway host for node [%s] info: %v", nodeid, err))
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
listenPort := logic.GetPeerListenPort(host)
|
||
extclient.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
|
||
extclient.Enabled = true
|
||
extclient.DeviceID = customExtClient.DeviceID
|
||
extclient.DeviceName = customExtClient.DeviceName
|
||
if customExtClient.IsAlreadyConnectedToInetGw {
|
||
slog.Warn("RAC/Client is already connected to internet gateway. this may mask their real IP address", "client IP", customExtClient.PublicEndpoint)
|
||
}
|
||
extclient.PublicEndpoint = customExtClient.PublicEndpoint
|
||
extclient.Country = customExtClient.Country
|
||
extclient.Location = customExtClient.Location
|
||
if extClientCreateRequiresJIT(r.Context(), isMaster, caller, extclient.Network, customExtClient) {
|
||
hasAccess, grant, err := logic.CheckJITAccess(r.Context(), extclient.Network, userName)
|
||
if err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
if !hasAccess {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(
|
||
errors.New("JIT access required: please request access from network admin"),
|
||
"forbidden"))
|
||
return
|
||
}
|
||
// Set JIT expiry time if grant exists (nil for admin users or when JIT not enabled)
|
||
if grant != nil {
|
||
extclient.JITExpiresAt = &grant.ExpiresAt
|
||
}
|
||
}
|
||
|
||
if extclient.DeviceID != "" {
|
||
// check for violations connecting from desktop app
|
||
staticNode := models.ConvertToStaticNode(extclient)
|
||
deviceInfo := logic.GetPostureCheckDeviceInfoByNode(r.Context(), &staticNode)
|
||
violations, _ := logic.CheckPostureViolations(r.Context(), deviceInfo, schema.NetworkID(extclient.Network))
|
||
if len(violations) > 0 {
|
||
logic.EmitNewPostureViolationEvents(r.Context(), nil, violations, deviceInfo, schema.NetworkID(extclient.Network))
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("posture check violations"), logic.Forbidden))
|
||
return
|
||
}
|
||
}
|
||
|
||
if len(extclient.PublicKey) == 0 {
|
||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to create extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
node.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
extclient.PrivateKey = privateKey.String()
|
||
extclient.PublicKey = privateKey.PublicKey().String()
|
||
} else if len(extclient.PrivateKey) == 0 && len(extclient.PublicKey) > 0 {
|
||
extclient.PrivateKey = "[ENTER PRIVATE KEY]"
|
||
}
|
||
if extclient.ExtraAllowedIPs == nil {
|
||
extclient.ExtraAllowedIPs = []string{}
|
||
}
|
||
|
||
parentNetwork := &schema.Network{Name: extclient.Network}
|
||
err = parentNetwork.Get(r.Context())
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to create extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
node.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
|
||
if extclient.ClientID == "" {
|
||
extclient.ClientID, err = logic.GenerateNodeName(r.Context(), extclient.Network)
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to create extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
node.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
}
|
||
|
||
networkOrch := orchestrator.GetRepository().NetworkOrchestrator()
|
||
var reservedIPv4, reservedIPv6 string
|
||
|
||
if extclient.Address == "" {
|
||
if parentNetwork.AddressRange != "" {
|
||
newAddress, err := networkOrch.AllocateExtclientIP(r.Context(), parentNetwork)
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to create extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
node.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
reservedIPv4 = newAddress.String()
|
||
extclient.Address = reservedIPv4
|
||
}
|
||
}
|
||
|
||
if extclient.Address6 == "" {
|
||
if parentNetwork.AddressRange6 != "" {
|
||
addr6, err := networkOrch.AllocateExtclientIPv6(r.Context(), parentNetwork)
|
||
if err != nil {
|
||
if reservedIPv4 != "" {
|
||
networkOrch.FreeIPv4Reservation(parentNetwork.ID, reservedIPv4)
|
||
}
|
||
slog.Error(
|
||
"failed to create extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
node.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
reservedIPv6 = addr6.String()
|
||
extclient.Address6 = reservedIPv6
|
||
}
|
||
}
|
||
|
||
extclient.LastModified = time.Now().Unix()
|
||
if err := logic.ApplyExtClientInternetEgressSelection(r.Context(), &extclient, nodeid, &customExtClient); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
||
return
|
||
}
|
||
err = logic.SaveExtClient(r.Context(), &extclient)
|
||
// Reservations are freed regardless of outcome: on success the DB is authoritative,
|
||
// on failure the IPs must be available for reallocation.
|
||
if reservedIPv4 != "" {
|
||
networkOrch.FreeIPv4Reservation(parentNetwork.ID, reservedIPv4)
|
||
}
|
||
if reservedIPv6 != "" {
|
||
networkOrch.FreeIPv6Reservation(parentNetwork.ID, reservedIPv6)
|
||
}
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to create extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
node.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
|
||
slog.Info(
|
||
"created extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
node.Network,
|
||
"clientid",
|
||
extclient.ClientID,
|
||
)
|
||
|
||
if extclient.DeviceID != "" {
|
||
err = logic.CleanupOtherExtclients(r.Context(), &extclient)
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to clean up older extclients",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
extclient.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
}
|
||
|
||
if extclient.RemoteAccessClientID != "" {
|
||
// if created by user from client app, log event
|
||
logic.LogEvent(r.Context(), &models.Event{
|
||
Action: schema.Connect,
|
||
Source: models.Subject{
|
||
ID: userName,
|
||
Name: userName,
|
||
Type: schema.UserSub,
|
||
},
|
||
TriggeredBy: userName,
|
||
Target: models.Subject{
|
||
ID: extclient.Network,
|
||
Name: extclient.Network,
|
||
Type: schema.NetworkSub,
|
||
Info: extclient,
|
||
},
|
||
NetworkID: schema.NetworkID(extclient.Network),
|
||
Origin: schema.ClientApp,
|
||
})
|
||
}
|
||
|
||
extclient.AllowedIPs = logic.GetExtclientAllowedIPs(r.Context(), extclient)
|
||
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(extclient)
|
||
|
||
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
||
go func(ctx context.Context) {
|
||
extUpdateMutex.Lock()
|
||
mq.PublishPeerUpdate(ctx, false)
|
||
extUpdateMutex.Unlock()
|
||
}(ctx)
|
||
}
|
||
|
||
// @Summary Update a config file
|
||
// @Router /api/extclients/{network}/{clientid} [put]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Param clientid path string true "Client ID"
|
||
// @Param body body models.CustomExtClient true "Custom ext client update"
|
||
// @Success 200 {object} models.ExtClient
|
||
// @Failure 500 {object} models.ErrorResponse
|
||
// @Failure 400 {object} models.ErrorResponse
|
||
// @Failure 403 {object} models.ErrorResponse
|
||
func updateExtClient(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
var params = mux.Vars(r)
|
||
|
||
var update models.CustomExtClient
|
||
//var oldExtClient models.ExtClient
|
||
var replacePeers bool
|
||
err := json.NewDecoder(r.Body).Decode(&update)
|
||
if err != nil {
|
||
logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
|
||
err.Error())
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
clientid := params["clientid"]
|
||
oldExtClient, err := logic.GetExtClientByName(r.Context(), clientid)
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to retrieve extclient",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"id",
|
||
clientid,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
if update.ClientID == "" {
|
||
update.ClientID = oldExtClient.ClientID
|
||
}
|
||
if oldExtClient.ClientID == update.ClientID {
|
||
if err := validateCustomExtClient(r.Context(), &update, false); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
} else {
|
||
if err := validateCustomExtClient(r.Context(), &update, true); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
}
|
||
|
||
var gateway models.EgressGatewayRequest
|
||
gateway.NetID = params["network"]
|
||
gateway.Ranges = update.ExtraAllowedIPs
|
||
err = logic.ValidateEgressRange(r.Context(), gateway.NetID, gateway.Ranges)
|
||
if err != nil {
|
||
logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
||
return
|
||
}
|
||
|
||
if update.PublicKey != oldExtClient.PublicKey {
|
||
//remove old peer entry
|
||
replacePeers = true
|
||
}
|
||
newclient := logic.UpdateExtClient(&oldExtClient, &update)
|
||
if err := logic.ApplyExtClientInternetEgressSelection(r.Context(), &newclient, newclient.IngressGatewayID, &update); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
||
return
|
||
}
|
||
if newclient.DeviceID != "" && newclient.Enabled {
|
||
// check for violations connecting from desktop app
|
||
staticNode := models.ConvertToStaticNode(newclient)
|
||
deviceInfo := logic.GetPostureCheckDeviceInfoByNode(r.Context(), &staticNode)
|
||
violations, _ := logic.CheckPostureViolations(r.Context(), deviceInfo, schema.NetworkID(newclient.Network))
|
||
if len(violations) > 0 {
|
||
logic.EmitNewPostureViolationEvents(r.Context(), nil, violations, deviceInfo, schema.NetworkID(newclient.Network))
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("posture check violations"), logic.Forbidden))
|
||
return
|
||
}
|
||
// JIT enforcement: Check if user has access (only for desktop app users)
|
||
hasAccess, jitGrant, err := logic.CheckJITAccess(r.Context(), newclient.Network, newclient.OwnerID)
|
||
if err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||
return
|
||
}
|
||
if !hasAccess {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(
|
||
errors.New("JIT access required: please request access from network admin"),
|
||
"forbidden"))
|
||
return
|
||
}
|
||
if jitGrant != nil {
|
||
newclient.JITExpiresAt = &jitGrant.ExpiresAt
|
||
}
|
||
|
||
}
|
||
if err := logic.DeleteExtClient(r.Context(), oldExtClient.Network, oldExtClient.ClientID, true); err != nil {
|
||
slog.Error(
|
||
"failed to delete ext client",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"id",
|
||
oldExtClient.ClientID,
|
||
"network",
|
||
oldExtClient.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
if err := logic.SaveExtClient(r.Context(), &newclient); err != nil {
|
||
slog.Error(
|
||
"failed to save ext client",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"id",
|
||
newclient.ClientID,
|
||
"network",
|
||
newclient.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
logger.Log(0, r.Header.Get("user"), "updated ext client", update.ClientID)
|
||
|
||
if newclient.DeviceID != "" && update.Enabled {
|
||
// user wants to enable this extclient, so delete all the other extclients.
|
||
err = logic.CleanupOtherExtclients(r.Context(), &newclient)
|
||
if err != nil {
|
||
slog.Error(
|
||
"failed to clean up older extclients",
|
||
"user",
|
||
r.Header.Get("user"),
|
||
"network",
|
||
oldExtClient.Network,
|
||
"error",
|
||
err,
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
}
|
||
|
||
newclient.AllowedIPs = logic.GetExtclientAllowedIPs(r.Context(), newclient)
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(newclient)
|
||
|
||
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
||
go func(ctx context.Context) {
|
||
if replacePeers || !update.Enabled {
|
||
if err := mq.PublishDeletedClientPeerUpdate(ctx, &oldExtClient); err != nil {
|
||
slog.Error("error deleting old ext peers", "error", err.Error())
|
||
}
|
||
}
|
||
extUpdateMutex.Lock()
|
||
mq.PublishPeerUpdate(ctx, false)
|
||
extUpdateMutex.Unlock()
|
||
}(ctx)
|
||
|
||
}
|
||
|
||
// @Summary Delete a config file
|
||
// @Router /api/extclients/{network}/{clientid} [delete]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Param clientid path string true "Client ID"
|
||
// @Success 200 {object} models.SuccessResponse
|
||
// @Failure 500 {object} models.ErrorResponse
|
||
// @Failure 403 {object} models.ErrorResponse
|
||
func deleteExtClient(w http.ResponseWriter, r *http.Request) {
|
||
// Set header
|
||
w.Header().Set("Content-Type", "application/json")
|
||
|
||
// get params
|
||
var params = mux.Vars(r)
|
||
clientid := params["clientid"]
|
||
network := params["network"]
|
||
extclient, err := logic.GetExtClient(r.Context(), clientid, network)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
logger.Log(0, r.Header.Get("user"),
|
||
"Deleted extclient client", params["clientid"], "from network", params["network"])
|
||
logic.ReturnSuccessResponse(w, r, params["clientid"]+" deleted.")
|
||
return
|
||
}
|
||
|
||
err = errors.New("Could not delete extclient " + params["clientid"])
|
||
logger.Log(0, r.Header.Get("user"),
|
||
fmt.Sprintf("failed to get extclient [%s],network [%s]: %v", clientid, network, err))
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
ingressnode, err := logic.GetNodeByID(extclient.IngressGatewayID)
|
||
if err != nil {
|
||
logger.Log(
|
||
0,
|
||
r.Header.Get("user"),
|
||
fmt.Sprintf(
|
||
"failed to get ingress gateway node [%s] info: %v",
|
||
extclient.IngressGatewayID,
|
||
err,
|
||
),
|
||
)
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
|
||
err = logic.DeleteExtClientAndCleanup(r.Context(), extclient)
|
||
if err != nil {
|
||
slog.Error("deleteExtClient: ", "Error", err.Error())
|
||
err = errors.New("Could not delete extclient " + params["clientid"])
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||
return
|
||
}
|
||
|
||
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
||
go func(ctx context.Context) {
|
||
if err := mq.PublishDeletedClientPeerUpdate(ctx, &extclient); err != nil {
|
||
slog.Error("error setting ext peers on " + ingressnode.ID.String() + ": " + err.Error())
|
||
}
|
||
}(ctx)
|
||
|
||
logger.Log(0, r.Header.Get("user"),
|
||
"Deleted extclient client", params["clientid"], "from network", params["network"])
|
||
logic.ReturnSuccessResponse(w, r, params["clientid"]+" deleted.")
|
||
}
|
||
|
||
// @Summary Bulk delete ext clients
|
||
// @Router /api/v1/extclients/{network}/bulk [delete]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Param body body models.BulkDeleteRequest true "List of ext client IDs to delete"
|
||
// @Success 202 {object} models.SuccessResponse
|
||
// @Failure 400 {object} models.ErrorResponse
|
||
func bulkDeleteExtClients(w http.ResponseWriter, r *http.Request) {
|
||
network := mux.Vars(r)["network"]
|
||
var req models.BulkDeleteRequest
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid request body: %w", err), logic.BadReq))
|
||
return
|
||
}
|
||
if len(req.IDs) == 0 {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("no ext client IDs provided"), logic.BadReq))
|
||
return
|
||
}
|
||
if err := (&schema.Network{Name: network}).Get(r.Context()); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("network %s not found", network), logic.BadReq))
|
||
return
|
||
}
|
||
user := r.Header.Get("user")
|
||
logic.ReturnAcceptedResponse(w, r, fmt.Sprintf("bulk delete of %d ext client(s) accepted", len(req.IDs)))
|
||
|
||
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
||
go func(ctx context.Context) {
|
||
deleted := 0
|
||
gwDeletedClients := make(map[string][]models.ExtClient)
|
||
for _, clientID := range req.IDs {
|
||
extclient, err := logic.GetExtClient(ctx, clientID, network)
|
||
if err != nil {
|
||
slog.Error("bulk extclient delete: client not found", "client_id", clientID, "network", network, "error", err)
|
||
continue
|
||
}
|
||
if err = logic.DeleteExtClientAndCleanup(ctx, extclient); err != nil {
|
||
slog.Error("bulk extclient delete: failed to delete", "client_id", clientID, "error", err)
|
||
continue
|
||
}
|
||
gwDeletedClients[extclient.IngressGatewayID] = append(gwDeletedClients[extclient.IngressGatewayID], extclient)
|
||
logic.LogEvent(ctx, &models.Event{
|
||
Action: schema.Delete,
|
||
Source: models.Subject{
|
||
ID: user,
|
||
Name: user,
|
||
Type: schema.UserSub,
|
||
},
|
||
TriggeredBy: user,
|
||
Target: models.Subject{
|
||
ID: extclient.ClientID,
|
||
Name: extclient.ClientID,
|
||
Type: schema.NetworkSub,
|
||
},
|
||
NetworkID: schema.NetworkID(network),
|
||
Origin: schema.Dashboard,
|
||
Diff: models.Diff{Old: extclient, New: nil},
|
||
})
|
||
logger.Log(0, user, "Deleted extclient", clientID, "from network", network)
|
||
deleted++
|
||
}
|
||
if deleted > 0 {
|
||
allNodes, _ := logic.GetAllNodes(ctx)
|
||
for gwNodeID, clients := range gwDeletedClients {
|
||
gwNode, err := logic.GetNodeByID(gwNodeID)
|
||
if err != nil {
|
||
slog.Error("bulk extclient delete: gw node not found", "node_id", gwNodeID, "error", err)
|
||
continue
|
||
}
|
||
gwHost := &schema.Host{ID: gwNode.HostID}
|
||
if err := gwHost.Get(ctx); err != nil {
|
||
slog.Error("bulk extclient delete: gw host not found", "host_id", gwNode.HostID, "error", err)
|
||
continue
|
||
}
|
||
go mq.PublishSingleHostPeerUpdate(ctx, gwHost, allNodes, nil, nil, clients, false, nil)
|
||
|
||
}
|
||
go mq.PublishPeerUpdate(ctx, false)
|
||
}
|
||
slog.Info("bulk extclient delete completed", "deleted", deleted, "total", len(req.IDs))
|
||
}(ctx)
|
||
}
|
||
|
||
// validateCustomExtClient Validates the extclient object
|
||
func validateCustomExtClient(ctx context.Context, customExtClient *models.CustomExtClient, checkID bool) error {
|
||
v := validator.New()
|
||
err := v.Struct(customExtClient)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
//validate clientid
|
||
if customExtClient.ClientID != "" {
|
||
if err := isValid(ctx, customExtClient.ClientID, checkID); err != nil {
|
||
return fmt.Errorf("client validation: %v", err)
|
||
}
|
||
}
|
||
//extclient.ClientID = customExtClient.ClientID
|
||
if len(customExtClient.PublicKey) > 0 {
|
||
if _, err := wgtypes.ParseKey(customExtClient.PublicKey); err != nil {
|
||
return errInvalidExtClientPubKey
|
||
}
|
||
//extclient.PublicKey = customExtClient.PublicKey
|
||
}
|
||
//validate extra ips
|
||
if len(customExtClient.ExtraAllowedIPs) > 0 {
|
||
for _, ip := range customExtClient.ExtraAllowedIPs {
|
||
if _, _, err := net.ParseCIDR(ip); err != nil {
|
||
return errInvalidExtClientExtraIP
|
||
}
|
||
}
|
||
//extclient.ExtraAllowedIPs = customExtClient.ExtraAllowedIPs
|
||
}
|
||
//validate DNS
|
||
if customExtClient.DNS != "" {
|
||
ips := strings.Split(customExtClient.DNS, ",")
|
||
for _, ip := range ips {
|
||
trimmedIp := strings.TrimSpace(ip)
|
||
if ip := net.ParseIP(trimmedIp); ip == nil {
|
||
return errInvalidExtClientDNS
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// isValid Checks if the clientid is valid
|
||
func isValid(ctx context.Context, clientid string, checkID bool) error {
|
||
if !validName(clientid) {
|
||
return errInvalidExtClientID
|
||
}
|
||
if checkID {
|
||
extclients, err := logic.GetAllExtClients(ctx)
|
||
if err != nil {
|
||
return fmt.Errorf("extclients isValid: %v", err)
|
||
}
|
||
for _, extclient := range extclients {
|
||
if clientid == extclient.ClientID {
|
||
return errDuplicateExtClientName
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// @Summary Bulk update ext client enabled status
|
||
// @Router /api/v1/extclients/{network}/bulk/status [put]
|
||
// @Tags Config Files
|
||
// @Security oauth
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param network path string true "Network ID"
|
||
// @Param body body models.BulkExtClientStatusUpdate true "Client IDs and desired enabled state"
|
||
// @Success 202 {object} models.SuccessResponse
|
||
// @Failure 400 {object} models.ErrorResponse
|
||
func bulkUpdateExtClientStatus(w http.ResponseWriter, r *http.Request) {
|
||
network := mux.Vars(r)["network"]
|
||
if err := (&schema.Network{Name: network}).Get(r.Context()); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("network %s not found", network), logic.BadReq))
|
||
return
|
||
}
|
||
var req models.BulkExtClientStatusUpdate
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid request body: %w", err), logic.BadReq))
|
||
return
|
||
}
|
||
if len(req.IDs) == 0 {
|
||
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("no ext client IDs provided"), logic.BadReq))
|
||
return
|
||
}
|
||
|
||
eventAction := schema.Connect
|
||
if !req.Enabled {
|
||
eventAction = schema.Disconnect
|
||
}
|
||
user := r.Header.Get("user")
|
||
logic.ReturnAcceptedResponse(w, r, fmt.Sprintf("bulk %s of %d ext client(s) accepted", eventAction, len(req.IDs)))
|
||
|
||
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
||
go func(ctx context.Context) {
|
||
updated := 0
|
||
for _, clientID := range req.IDs {
|
||
client, err := logic.GetExtClient(ctx, clientID, network)
|
||
if err != nil {
|
||
slog.Error("bulk extclient status: client not found", "client_id", clientID, "error", err)
|
||
continue
|
||
}
|
||
if client.Enabled == req.Enabled {
|
||
continue
|
||
}
|
||
oldClient := client
|
||
if _, err := logic.ToggleExtClientConnectivity(ctx, &client, req.Enabled); err != nil {
|
||
slog.Error("bulk extclient status: failed to toggle", "client_id", clientID, "error", err)
|
||
continue
|
||
}
|
||
if !req.Enabled {
|
||
if err := mq.PublishDeletedClientPeerUpdate(ctx, &client); err != nil {
|
||
slog.Error("bulk extclient status: error publishing peer update", "client_id", clientID, "error", err)
|
||
}
|
||
}
|
||
logic.LogEvent(ctx, &models.Event{
|
||
Action: eventAction,
|
||
Source: models.Subject{
|
||
ID: user,
|
||
Name: user,
|
||
Type: schema.UserSub,
|
||
},
|
||
TriggeredBy: user,
|
||
Target: models.Subject{
|
||
ID: client.ClientID,
|
||
Name: client.ClientID,
|
||
Type: schema.NetworkSub,
|
||
Info: client,
|
||
},
|
||
NetworkID: schema.NetworkID(network),
|
||
Origin: schema.Dashboard,
|
||
Diff: models.Diff{Old: oldClient, New: client},
|
||
})
|
||
updated++
|
||
}
|
||
if updated > 0 {
|
||
mq.PublishPeerUpdate(ctx, false)
|
||
}
|
||
slog.Info("bulk extclient status completed", "action", eventAction, "updated", updated, "total", len(req.IDs))
|
||
}(ctx)
|
||
}
|
||
|
||
// extClientCreateRequiresJIT reports whether create must verify JIT access.
|
||
// Only client-app requests (device_id / remote_access_client_id) are enforced.
|
||
// Dashboard config-file creates skip JIT. Master key skips. When JIT groups are set,
|
||
// users outside that allowlist are not subject.
|
||
func extClientCreateRequiresJIT(ctx context.Context, isMaster bool, caller *schema.User, networkID string, custom models.CustomExtClient) bool {
|
||
if custom.DeviceID == "" && custom.RemoteAccessClientID == "" {
|
||
return false
|
||
}
|
||
if isMaster {
|
||
return false
|
||
}
|
||
return logic.UserSubjectToNetworkJIT(ctx, networkID, caller)
|
||
}
|