mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
* wip(go): return auth token with user on create org owner; * wip(go): prevent use of default tenant slug ; * wip(go): upsert settings for tenant in ctx; * wip(go): use passed ctx when updating user; remove password validation from ValidateUser (already validated in UpdateUser); * wip(go): qualify all tenant id usages with table name; * wip(go): use passed ctx * wip(go): return old enrollment key type on create; * wip(go): prevent null networks return; * wip(go): wire ctx in dns/nameserver functions; wire ctx in host registration functions; * wip(go): wire ctx in node functions; * wip(go): wire ctx in node functions; * wip(go): wire ctx in extclient functions; * wip(go): remove not needed field; * wip(go): use passed ctx; * wip(go): use unique index instead of primary key; * wip(go): wire ctx in extclient apis; * wip(go): wire ctx in extclient apis; * wip(go): remove org id check from global scoped get tenant api; * wip(go): add scope slug to discover response; * wip(go): wire ctx in get users; * wip(go): set tenant id in posture check violations; * wip(go): upsert membership instead of user; * wip(go): pass correct len; * wip(go): optimize (single loop); * wip(go): unify todos; * wip(go): add todo; * wip(go): wire context for network get; * wip(go): wire context; * wip(go): wire context; * wip(go): wire ctx when getting tenant superadmin; * wip(go): add network creator to network admin group; * wip(go): check for user membership on sync; * wip(go): add methods with strict membership check; * wip(go): remove unwanted feature flags; * wip(go): report per-tenant usage during license validation; * wip(go): move email setting from tenant to org; * wip(go): pass org scope to email init; * wip(go): remove old tenant email settings usages; use org settings to get email settings * wip(go): use get email settings from org settings; * wip(go): remove email settings migration; * wip(go): migrate email settings from server to org settings; * wip(go): assign global network admin group membership only for super-admin and admins; * wip(go): assign global network admin group membership only for super-admin and admins; * wip(go): add membership alias to filter; * wip(go): skip clearing local license validation response cache; * wip(go): extend license validation response for multi-tenancy; * wip(go): add functions to get sole tenant and org; * wip(go): use tenant-specific feature flags; * wip(go): add tenant status type; * wip(go): remove old hook comment; * wip(go): expect ctx during license validation; * wip(go): always create default org and tenant for ce; * wip(go): add tenant-specific check for license validation error; * wip(go): cache license validation response in memory; * wip(go): add todo to handle multiple-tenant for integration pull; * wip(go): pass ctx during license validation; * wip(go): add todo for making changes later; run hook separately for each tenant; * wip(go): return specific errors when license validation response not in cache; * wip(go): use retryable.httpclient for calling license validation api; * wip(go): process license validation; * wip(go): license validation function; * wip(go): migration on license validation; * wip(go): org settings not expected; * wip(go): remove expiry from license validation response; * wip(go): add limits and limits enforcement to license validation response; * wip(go): remove expiry usages; * wip(go): add arg to clear cache; * wip(go): add arg to clear cache; * wip(go): skip license validation if cached resp; * wip(go): allow org_id and tenant_id query params; * wip(go): upsert tenant membership on sync; * wip(go): pass ctx to delete network roles; * wip(go): create tenant membership on org owner creation on ce; * wip(go): add tenant orchestrator; * wip(go): use tenant orchestrator; * wip(go): add default settings for tenant; * wip(go): make org owner superadmin on all tenants; * wip(go): simplify SyncOrgAndTenants; * wip(go): retry license validation only 3 times * wip(go): store invalid err; * wip(go): add function to check if limits are to be enforced on a tenant; * wip(go): pass tenant scope if possible when checking for license validation error; * wip(go): remove merge errors; * wip(go): add functions for limit checks; * wip(go): wire host limit check; * wip(go): wire ingress limit check; * wip(go): wire egress limit check; * wip(go): wire client limit check; * wip(go): wire network limit check; * wip(go): wire user limit check; * wip(go): use correct errors; * wip(go): skip telemetry for msps; * wip(go): disable telemetry on msp tenants; * wip(go): err check; * wip(go): uncomment log event; * wip(go): get sso state to get scope for non-headless oauth login; * wip(go): inline delete pending user; * wip(go): allow both org and tenant scope for pending users table; * wip(go): add migration for pending user table during set tenant id and rekey org/tenant id; * wip(go): update queries to use new scope fields in pending users table; * wip(go): allow both org and tenant scope for user invites table; * wip(go): update queries to use new scope fields; * wip(go): add migration for rekey-ing org/tenant id and setting tenant id; * wip(go): inline delete user invite; * wip(go): set scope when creating invite; * wip(go): get user count with membership is ctx is tenant scoped; * wip(go): add methods to upsert and delete org memberships; * wip(go): add global valid tenant and org roles map; * wip(go): add scope query in user invite get by email; * wip(go): add support for upsert and delete of org and tenant membership both; * wip(go): infer scope for user invite apis; * wip(go): wire ctx in get user invite by email; * wip(go): allow no scope headers to be passed in infer scope apis; * wip(go): add validation and checks for org-scoped invite api handlers; * wip(go): wire ctx to is pending user check; * wip(go): infer scope for pending user apis; * wip(go): set default role based on ctx scope; * wip(go): move mfa to tenant membership table; move account status to membership tables; * wip(go): migrate account status and mfa settings; * wip(go): update account status on sync; * wip(go): use update mfa to update mfa setting; * wip(go): set new membership settings on user creation; * wip(go): use membership where required; * wip(go): wire ctx on key unlink refs; * wip(go): use r.Context directly without reinitializing db; * wip(go): wire ctx in acls; * wip(go): add tenant scoped primary key constructor; * wip(go): use tenant scoped primary key for acls, dns, extclients and tags; * wip(go): wire ctx for get groups in network; * wip(go): wire ctx for get groups; * wip(go): scope non-uuid group ids by tenant id; * wip(go): migrate non-uuid group ids to scoped ids; * wip(go): don't expect scope id in invite validation and signup apis; * wip(go): wire ctx for get matching user; allow org scope when querying by external idp id; * wip(go): add mfa to org; * wip(go): scope preauth token; * wip(go): add fallback for older auth tokens; * wip(go): wire ctx for preauth token creation; * wip(go): migrate mfa settings; * wip(go): resolve mfa settings for inherited auth; * wip(go): update mfa for org; * wip(go): infer scope for totp apis; * wip(go): use ctx when passed; * wip(go): infer scope for list user apis; * wip(go): ensure set primary keys don't cause errors during fetch; * wip(go): make primary key for integrations uuid and store provider in separate key; * wip(go): migrate integration ids; * wip(go): use provider instead of id everywhere; * wip(go): remove gorm column; * wip(go): wire ctx for igw egress creation; * wip(go): wire ctx for igw egress update; * wip(go): wire ctx for host registration; * wip(go): wire ctx for host registration; * wip(go): merge errors; * wip(go): merge errors; * wip(go): wire ctx; remove default scope usages; * wip(go): use get with membership; * wip(go): org scope license validation; * wip(go): remove debug log; * wip(go): remove scoping on list roles; * wip(go): ignore invite creation if already exists; * wip(go): add is msp func to check if deployment is msp deployment; * wip(go): ensure username is email for msp users; * wip(go): add email validated check on user table; * wip(go): mark email validated when oauth user; * wip(go): handle gorm error; * wip(go): add tenant id to siem grpc schema; * wip(go): read tenant from scoped ctx; * wip(go): pass scoped ctx to siem grpc client; * wip(go): remove unused publisher; * wip(go): publish all tenant exporter updates on pull; * wip(go): fix wg usage; * wip(go): tenant scope the ctx when getting server info; * wip(go): add tenant id to enrollment key token; * wip(go): make host register tenant scope; * wip(go): add default settings; * wip(go): use correct checks for is msp tenant; * wip(go): add email validation extension; * wip(go): add pages for validation check; * wip(go): use extension for validating user; * wip(go): add invite type; * wip(go): create only one validation invite per user email; * wip(go): add validate email handler; * wip(go): add email builder; * wip(go): add user invite type migration logic; * wip(go): only log email send failures; * wip(go): add tenant id to flows table; * wip(go): add func to check if connected to clickhouse; * wip(go): migrate clickhouse; * wip(go): add tenant id to mdm and edr state; * wip(go): add delete methods for tables; * wip(go): add tenant teardown method; * wip(go): handle tenant deletion; * wip(go): clean up flow logs on tenant deletion; * wip(go): add update org membership role id method; * wip(go): add transfer org ownership api; * wip(go): move audit logs retention period to org settings; * wip(go):get retention period from org settings; * wip(go): remove old audit logs usage; * wip(go): migrate audit logs retention period; * wip(go): unmarshal decodes nums as float64; * wip(go): run flow logs cleanup for all tenants in one go; * wip(go): add audit logs retention period and smtp settings to tenant settings table for non-msp tenants; * wip(go): resolve retention period and smtp settings correctly; * wip(go): handle retention period and smtp settings updates and fetches correctly; * wip(go): overlay retention period and smtp settings on server settings;; * wip(go): upsert overlayed settings; * wip(go): add azure tenant id to org settings; * wip(go): tenant scope list roles; * wip(go): add noop creds; * wip(go): use first instead of find; * wip(go): use correct roles when transfering ownership; * wip(go): delete access tokens only if tenant scoped; * wip(go): make delete user work for both org and tenant scope; * wip(go): delete extclients only if tenant scoped; * wip(go): check if caller is allowed to delete user; * wip(go): infer scope for update user; * wip(go): prevent org-owner elevation; * wip(go): permissions check for update; * wip(go): delete user access tokens only if tenant scope; * wip(go): delete extclients only if tenant scope; * wip(go): move upsert membership in upsert user; * wip(go): set api host when constructing server config; * wip(go): fix static errors; * wip(go): fix test cases; * wip(go): use correct get ns order; * wip(go): use correct get pending host order; * wip(go): use correct get access token order; * wip(go): use correct get event order; * wip(go): return error response on emqx user creation error; * wip(go): store correct value in cache; * wip(go): return better error response; * wip(go): return better error response; * wip(go): use cache when listing egress acls; * wip(go): wire ctx in UserHasNetworkGroupAccess; * wip(go): remove double scope wrapping; * wip(go): add security check for list and get org; * wip(go): ratelimit discover api; * wip(go): expect sole org id in get and create org owner; * wip(go): wrap create org owner in tx;
611 lines
18 KiB
Go
611 lines
18 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
"github.com/gravitl/netmaker/db"
|
|
"github.com/gravitl/netmaker/logger"
|
|
"github.com/gravitl/netmaker/logic"
|
|
"github.com/gravitl/netmaker/middleware"
|
|
"github.com/gravitl/netmaker/models"
|
|
"github.com/gravitl/netmaker/mq"
|
|
"github.com/gravitl/netmaker/schema"
|
|
"github.com/gravitl/netmaker/scope"
|
|
"github.com/gravitl/netmaker/servercfg"
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
func egressHandlers(r *mux.Router) {
|
|
r.HandleFunc("/api/v1/egress/presets", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getEgressPresets)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v1/egress", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(createEgress)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/v1/egress", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(listEgress)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v1/egress", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(updateEgress)))).Methods(http.MethodPut)
|
|
r.HandleFunc("/api/v1/egress", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(deleteEgress)))).Methods(http.MethodDelete)
|
|
}
|
|
|
|
// @Summary List egress domain presets
|
|
// @Router /api/v1/egress/presets [get]
|
|
// @Tags Egress
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
func getEgressPresets(w http.ResponseWriter, r *http.Request) {
|
|
presets := []models.EgressPresetApp{}
|
|
if servercfg.IsPro {
|
|
presets = logic.ListEgressPresets()
|
|
}
|
|
logic.ReturnSuccessResponseWithJson(w, r, map[string]any{
|
|
"presetApps": presets,
|
|
}, "fetched egress preset catalog")
|
|
}
|
|
|
|
// @Summary Create Egress Resource
|
|
// @Router /api/v1/egress [post]
|
|
// @Tags Egress
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body models.EgressReq true "Egress request data"
|
|
// @Success 200 {object} schema.Egress
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func createEgress(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var req models.EgressReq
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "error decoding request body: ",
|
|
err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if err := logic.ValidateEgressReqProLimits(&req); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if req.PresetID != "" {
|
|
if err := logic.ApplyEgressPresetToEgressReq(&req); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
}
|
|
normDomains, err := logic.NormalizeEgressReqDomains(req.Domains)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
inetGw := logic.IsEgressReqInternetGateway(&req)
|
|
if inetGw {
|
|
normDomains = nil
|
|
}
|
|
req.Domains = normDomains
|
|
var resolvedCIDRs []string
|
|
if req.PresetID != "" {
|
|
if p, ok := logic.GetEgressPresetByID(req.PresetID); ok && logic.PresetYieldsAWSIPRanges(p) {
|
|
if c, err := logic.ResolveAWSEgressPresetCIDRs(http.DefaultClient, p); err == nil && len(c) > 0 {
|
|
resolvedCIDRs = c
|
|
} else if err != nil {
|
|
logger.Log(0, "aws preset ip fetch failed:", req.PresetID, err.Error())
|
|
}
|
|
}
|
|
}
|
|
var egressRange string
|
|
egressType := logic.InferEgressType(&req)
|
|
if inetGw {
|
|
egressType = schema.EgressTypeInternet
|
|
egressRange = "*"
|
|
req.Domains = nil
|
|
req.PresetID = ""
|
|
} else if len(normDomains) > 0 {
|
|
egressRange = ""
|
|
if egressType == "" || egressType == schema.EgressTypeCIDR {
|
|
if req.PresetID != "" {
|
|
egressType = schema.EgressTypeApp
|
|
} else {
|
|
egressType = schema.EgressTypeDomain
|
|
}
|
|
}
|
|
} else if req.Range != "" {
|
|
egressRange, err = logic.NormalizeCIDR(req.Range)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
}
|
|
network := &schema.Network{Name: req.Network}
|
|
err = network.Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
e := schema.Egress{
|
|
ID: uuid.New().String(),
|
|
TenantID: scope.ID(r.Context()),
|
|
Name: req.Name,
|
|
Network: req.Network,
|
|
Description: req.Description,
|
|
Type: egressType,
|
|
Range: egressRange,
|
|
Nat: req.Nat,
|
|
Mode: req.Mode,
|
|
Nodes: make(datatypes.JSONMap),
|
|
Tags: make(datatypes.JSONMap),
|
|
PresetID: req.PresetID,
|
|
Status: true,
|
|
CreatedBy: r.Header.Get("user"),
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
logic.ApplyConfiguredDomainsToEgress(&e, normDomains)
|
|
if len(resolvedCIDRs) > 0 {
|
|
logic.SetEgressDomainAnsForDomains(&e, normDomains, resolvedCIDRs)
|
|
}
|
|
if len(req.Tags) > 0 {
|
|
for tagID, metric := range req.Tags {
|
|
e.Tags[tagID] = metric
|
|
}
|
|
e.Nodes = make(datatypes.JSONMap)
|
|
} else {
|
|
for nodeID, metric := range req.Nodes {
|
|
e.Nodes[nodeID] = metric
|
|
}
|
|
}
|
|
if logic.EgressLimitExceeded(r.Context()) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(logic.ErrEgressLimitExceeded, logic.Forbidden))
|
|
return
|
|
}
|
|
if err := logic.ValidateEgressReq(r.Context(), &e); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if err := logic.AssignVirtualRangeToEgress(network, &e); err != nil {
|
|
logger.Log(0, "error assigning virtual range to egress: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
logger.Log(1, fmt.Sprintf("createEgress: after AssignVirtualRangeToEgress, e.VirtualRange = '%s', e.Mode = '%s', e.Nat = %v", e.VirtualRange, e.Mode, e.Nat))
|
|
err = e.Create(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error creating egress resource"+err.Error()), "internal"),
|
|
)
|
|
return
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Create,
|
|
Source: models.Subject{
|
|
ID: r.Header.Get("user"),
|
|
Name: r.Header.Get("user"),
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: r.Header.Get("user"),
|
|
Target: models.Subject{
|
|
ID: e.ID,
|
|
Name: e.Name,
|
|
Type: schema.EgressSub,
|
|
},
|
|
NetworkID: schema.NetworkID(e.Network),
|
|
Origin: schema.Dashboard,
|
|
})
|
|
// for nodeID := range e.Nodes {
|
|
// node, err := logic.GetNodeByID(nodeID)
|
|
// if err != nil {
|
|
// logic.AddEgressInfoToNode(&node, e)
|
|
// logic.UpsertNode(&node)
|
|
// }
|
|
|
|
// }
|
|
if len(normDomains) > 0 && !logic.HasEgressDomainAns(e) {
|
|
if req.Nodes != nil {
|
|
for nodeID := range req.Nodes {
|
|
node, err := logic.GetNodeByID(nodeID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
host := &schema.Host{
|
|
ID: node.HostID,
|
|
}
|
|
err = host.Get(r.Context())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, dom := range normDomains {
|
|
mq.HostUpdate(&models.HostUpdate{
|
|
Action: models.EgressUpdate,
|
|
Host: *host,
|
|
EgressDomain: models.EgressDomain{
|
|
ID: e.ID,
|
|
Host: *host,
|
|
Node: node,
|
|
Domain: dom,
|
|
},
|
|
Node: node,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
}
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, e, "created egress resource")
|
|
}
|
|
|
|
// @Summary List Egress Resources
|
|
// @Router /api/v1/egress [get]
|
|
// @Tags Egress
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param network query string true "Network identifier"
|
|
// @Success 200 {array} schema.Egress
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func listEgress(w http.ResponseWriter, r *http.Request) {
|
|
|
|
network := r.URL.Query().Get("network")
|
|
if network == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), "badrequest"))
|
|
return
|
|
}
|
|
e := schema.Egress{Network: network}
|
|
list, err := e.ListByNetwork(db.WithContext(r.Context()))
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error listing egress resource"+err.Error()), "internal"),
|
|
)
|
|
return
|
|
}
|
|
logic.ReturnSuccessResponseWithJson(w, r, list, "fetched egress resource list")
|
|
}
|
|
|
|
// @Summary Update Egress Resource
|
|
// @Router /api/v1/egress [put]
|
|
// @Tags Egress
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body models.EgressReq true "Egress request data"
|
|
// @Success 200 {object} schema.Egress
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func updateEgress(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var req models.EgressReq
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "error decoding request body: ",
|
|
err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if err := logic.ValidateEgressReqProLimits(&req); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if req.PresetID != "" {
|
|
if err := logic.ApplyEgressPresetToEgressReq(&req); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
}
|
|
normDomains, err := logic.NormalizeEgressReqDomains(req.Domains)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
inetGw := logic.IsEgressReqInternetGateway(&req)
|
|
if inetGw {
|
|
normDomains = nil
|
|
}
|
|
req.Domains = normDomains
|
|
network := &schema.Network{Name: req.Network}
|
|
err = network.Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
var egressRange string
|
|
egressType := logic.InferEgressType(&req)
|
|
if inetGw {
|
|
egressType = schema.EgressTypeInternet
|
|
egressRange = "*"
|
|
req.Domains = nil
|
|
req.PresetID = ""
|
|
} else if len(normDomains) > 0 {
|
|
egressRange = ""
|
|
if egressType == "" || egressType == schema.EgressTypeCIDR {
|
|
if req.PresetID != "" {
|
|
egressType = schema.EgressTypeApp
|
|
} else {
|
|
egressType = schema.EgressTypeDomain
|
|
}
|
|
}
|
|
} else if req.Range != "" {
|
|
egressRange, err = logic.NormalizeCIDR(req.Range)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
}
|
|
|
|
e := schema.Egress{ID: req.ID}
|
|
err = e.Get(db.WithContext(r.Context()))
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
oldConfigured := logic.ConfiguredDomainsForEgress(e)
|
|
oldPresetID := e.PresetID
|
|
oldMode := e.Mode
|
|
oldStatus := e.Status
|
|
oldRoutingNodes := make(map[string]struct{}, len(e.Nodes))
|
|
for nodeID := range e.Nodes {
|
|
oldRoutingNodes[nodeID] = struct{}{}
|
|
}
|
|
|
|
e.Range = egressRange
|
|
e.Type = egressType
|
|
event := &models.Event{
|
|
Action: schema.Update,
|
|
Source: models.Subject{
|
|
ID: r.Header.Get("user"),
|
|
Name: r.Header.Get("user"),
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: r.Header.Get("user"),
|
|
Target: models.Subject{
|
|
ID: e.ID,
|
|
Name: e.Name,
|
|
Type: schema.EgressSub,
|
|
},
|
|
Diff: models.Diff{
|
|
Old: e,
|
|
},
|
|
NetworkID: schema.NetworkID(e.Network),
|
|
Origin: schema.Dashboard,
|
|
}
|
|
e.Nodes = make(datatypes.JSONMap)
|
|
e.Tags = make(datatypes.JSONMap)
|
|
if len(req.Tags) > 0 {
|
|
for tagID, metric := range req.Tags {
|
|
e.Tags[tagID] = metric
|
|
}
|
|
e.Nodes = make(datatypes.JSONMap)
|
|
} else {
|
|
for nodeID, metric := range req.Nodes {
|
|
e.Nodes[nodeID] = metric
|
|
}
|
|
}
|
|
if !logic.EgressDomainsEqual(oldConfigured, normDomains) {
|
|
logic.ClearEgressDomainAns(&e)
|
|
}
|
|
e.Description = req.Description
|
|
e.Name = req.Name
|
|
logic.ApplyConfiguredDomainsToEgress(&e, normDomains)
|
|
e.Status = req.Status
|
|
if req.PresetID != "" {
|
|
e.PresetID = req.PresetID
|
|
}
|
|
if !req.Nat {
|
|
e.Nat = false
|
|
e.Mode = schema.DisabledNAT
|
|
e.VirtualRange = ""
|
|
} else if req.Mode == schema.VirtualNAT {
|
|
e.Nat = true
|
|
e.Mode = schema.VirtualNAT
|
|
} else {
|
|
e.Nat = true
|
|
e.Mode = schema.DirectNAT
|
|
e.VirtualRange = ""
|
|
}
|
|
presetChanged := req.PresetID != "" && req.PresetID != oldPresetID
|
|
domainsChanged := !logic.EgressDomainsEqual(oldConfigured, normDomains)
|
|
if req.PresetID != "" && (presetChanged || domainsChanged) {
|
|
if p, ok := logic.GetEgressPresetByID(req.PresetID); ok && logic.PresetYieldsAWSIPRanges(p) {
|
|
if c, err := logic.ResolveAWSEgressPresetCIDRs(http.DefaultClient, p); err == nil && len(c) > 0 {
|
|
logic.SetEgressDomainAnsForDomains(&e, normDomains, c)
|
|
} else if err != nil {
|
|
logger.Log(0, "aws preset ip fetch failed:", req.PresetID, err.Error())
|
|
}
|
|
}
|
|
}
|
|
e.UpdatedAt = time.Now().UTC()
|
|
if err := logic.ValidateEgressReq(r.Context(), &e); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if e.Nat && e.Mode == schema.VirtualNAT {
|
|
if (oldMode != schema.VirtualNAT) || (e.VirtualRange == "") {
|
|
if err := logic.AssignVirtualRangeToEgress(network, &e); err != nil {
|
|
logger.Log(0, "error assigning virtual range to egress: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build update map with all fields including zero values
|
|
// GORM's Updates(&e) doesn't update zero values, so we use a map explicitly
|
|
updateMap := map[string]any{
|
|
"name": e.Name,
|
|
"description": e.Description,
|
|
"egress_type": e.Type,
|
|
"range": e.Range,
|
|
"domains": e.Domains,
|
|
"nat": e.Nat,
|
|
"mode": e.Mode,
|
|
"status": e.Status,
|
|
"nodes": e.Nodes,
|
|
"tags": e.Tags,
|
|
"domain_ans_by_domain": e.DomainAnsByDomain,
|
|
"virtual_range": e.VirtualRange,
|
|
"preset_id": e.PresetID,
|
|
"updated_at": e.UpdatedAt,
|
|
}
|
|
|
|
// Perform single update with all fields including zero values
|
|
err = db.FromContext(r.Context()).Table(e.Table()).Where("id = ?", e.ID).Updates(updateMap).Error
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error updating egress resource: "+err.Error()), "internal"),
|
|
)
|
|
return
|
|
}
|
|
event.Diff.New = e
|
|
logic.LogEvent(r.Context(), event)
|
|
|
|
internetRoutingChanged := false
|
|
if logic.IsEgressInternetGateway(e) {
|
|
newRoutingNodes := make(map[string]struct{}, len(e.Nodes))
|
|
for nodeID := range e.Nodes {
|
|
newRoutingNodes[nodeID] = struct{}{}
|
|
}
|
|
internetRoutingChanged = len(oldRoutingNodes) != len(newRoutingNodes)
|
|
if !internetRoutingChanged {
|
|
for id := range oldRoutingNodes {
|
|
if _, ok := newRoutingNodes[id]; !ok {
|
|
internetRoutingChanged = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if internetRoutingChanged {
|
|
go func(eg schema.Egress) {
|
|
logic.RebindInternetEgressClients(r.Context(), eg)
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
_ = mq.PublishPeerUpdate(ctx, false)
|
|
}(e)
|
|
}
|
|
}
|
|
if len(normDomains) > 0 && !logic.HasEgressDomainAns(e) {
|
|
if req.Nodes != nil {
|
|
for nodeID := range req.Nodes {
|
|
node, err := logic.GetNodeByID(nodeID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
host := &schema.Host{
|
|
ID: node.HostID,
|
|
}
|
|
err = host.Get(r.Context())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, dom := range normDomains {
|
|
mq.HostUpdate(&models.HostUpdate{
|
|
Action: models.EgressUpdate,
|
|
Host: *host,
|
|
EgressDomain: models.EgressDomain{
|
|
ID: e.ID,
|
|
Host: *host,
|
|
Node: node,
|
|
Domain: dom,
|
|
},
|
|
Node: node,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Internet egress disabled: keep sticky selection, but push exit clients first so
|
|
// they fail open (drop full tunnel) before the global peer update.
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
if oldStatus && !e.Status && logic.IsEgressInternetGateway(e) {
|
|
clients := logic.ListNodesBySelectedInternetEgress(r.Context(), e.Network, e.ID)
|
|
go func(clients []models.Node) {
|
|
_ = mq.PublishPeerUpdatesForExitClientsFirst(ctx, clients)
|
|
}(clients)
|
|
} else if !internetRoutingChanged {
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
}
|
|
logic.ReturnSuccessResponseWithJson(w, r, e, "updated egress resource")
|
|
}
|
|
|
|
// @Summary Delete Egress Resource
|
|
// @Router /api/v1/egress [delete]
|
|
// @Tags Egress
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param id query string true "Egress resource ID"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func deleteEgress(w http.ResponseWriter, r *http.Request) {
|
|
|
|
id := r.URL.Query().Get("id")
|
|
if id == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
|
|
return
|
|
}
|
|
e := schema.Egress{ID: id}
|
|
err := e.Get(db.WithContext(r.Context()))
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
err = e.Delete(db.WithContext(r.Context()))
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
|
return
|
|
}
|
|
if logic.IsEgressInternetGateway(e) {
|
|
logic.ClearNodesSelectedInternetEgress(r.Context(), e.ID, e.Network)
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Delete,
|
|
Source: models.Subject{
|
|
ID: r.Header.Get("user"),
|
|
Name: r.Header.Get("user"),
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: r.Header.Get("user"),
|
|
Target: models.Subject{
|
|
ID: e.ID,
|
|
Name: e.Name,
|
|
Type: schema.EgressSub,
|
|
},
|
|
NetworkID: schema.NetworkID(e.Network),
|
|
Origin: schema.Dashboard,
|
|
Diff: models.Diff{
|
|
Old: e,
|
|
New: nil,
|
|
},
|
|
})
|
|
// delete related acl policies
|
|
acls := logic.ListAcls(r.Context())
|
|
for _, acl := range acls {
|
|
|
|
for i := len(acl.Dst) - 1; i >= 0; i-- {
|
|
if acl.Dst[i].ID == models.EgressID && acl.Dst[i].Value == id {
|
|
acl.Dst = append(acl.Dst[:i], acl.Dst[i+1:]...)
|
|
}
|
|
}
|
|
if len(acl.Dst) == 0 {
|
|
logic.DeleteAcl(r.Context(), acl)
|
|
} else {
|
|
logic.UpsertAcl(r.Context(), acl)
|
|
}
|
|
}
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted egress resource")
|
|
}
|