Files
Vishal Dalwadi 132aee0bb3 NM-341-v6: AMB Multi-tenancy Management (#4101)
* 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;
2026-08-05 21:25:02 +08:00

489 lines
17 KiB
Go

package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/gravitl/netmaker/db"
dbtypes "github.com/gravitl/netmaker/db/types"
"github.com/gravitl/netmaker/orchestrator"
"github.com/gravitl/netmaker/schema"
"golang.org/x/exp/slog"
"gorm.io/gorm"
"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/scope"
)
func networkHandlers(r *mux.Router) {
r.HandleFunc("/api/networks", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getNetworks)))).
Methods(http.MethodGet)
r.HandleFunc("/api/v1/networks/stats", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getNetworksStats)))).
Methods(http.MethodGet)
r.HandleFunc("/api/networks", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(createNetwork)))).
Methods(http.MethodPost)
r.HandleFunc("/api/networks/{networkname}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getNetwork)))).
Methods(http.MethodGet)
r.HandleFunc("/api/networks/{networkname}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(deleteNetwork)))).
Methods(http.MethodDelete)
r.HandleFunc("/api/networks/{networkname}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(updateNetwork)))).
Methods(http.MethodPut)
r.HandleFunc("/api/networks/{networkname}/egress_routes", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getNetworkEgressRoutes))))
}
// @Summary Lists all networks
// @Router /api/networks [get]
// @Tags Networks
// @Security oauth
// @Produce json
// @Success 200 {array} schema.Network
// @Failure 500 {object} models.ErrorResponse
func getNetworks(w http.ResponseWriter, r *http.Request) {
var err error
allnetworks, err := (&schema.Network{}).ListAll(r.Context())
if err != nil {
slog.Error("failed to fetch networks", "error", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if r.Header.Get("ismaster") != "yes" {
username := r.Header.Get("user")
user := &schema.User{Username: username}
err = user.Get(r.Context())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
allnetworks = logic.FilterNetworksByRole(r.Context(), allnetworks, user)
}
logger.Log(2, r.Header.Get("user"), "fetched networks.")
logic.SortNetworks(allnetworks[:])
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(allnetworks)
}
// @Summary Lists all networks with stats
// @Router /api/v1/networks/stats [get]
// @Tags Networks
// @Security oauth
// @Produce json
// @Success 200 {array} schema.Network
// @Failure 500 {object} models.ErrorResponse
func getNetworksStats(w http.ResponseWriter, r *http.Request) {
var err error
allnetworks, err := (&schema.Network{}).ListAll(r.Context())
if err != nil {
slog.Error("failed to fetch networks", "error", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if r.Header.Get("ismaster") != "yes" {
username := r.Header.Get("user")
user := &schema.User{Username: username}
err = user.GetWithMembership(r.Context())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
allnetworks = logic.FilterNetworksByRole(r.Context(), allnetworks, user)
}
allNodes, err := logic.GetAllNodes(r.Context())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
type networkStatResp struct {
Network *schema.Network
Hosts int
}
var netstats []networkStatResp
logic.SortNetworks(allnetworks[:])
for _, network := range allnetworks {
netstats = append(netstats, networkStatResp{
Network: &network,
Hosts: len(logic.GetNetworkNodesMemory(allNodes, network.Name)),
})
}
logger.Log(2, r.Header.Get("user"), "fetched networks.")
logic.ReturnSuccessResponseWithJson(w, r, netstats, "fetched networks with stats")
}
// @Summary Get a network
// @Router /api/networks/{networkname} [get]
// @Tags Networks
// @Security oauth
// @Param networkname path string true "Network name"
// @Produce json
// @Success 200 {object} schema.Network
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func getNetwork(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
netname := params["networkname"]
network := &schema.Network{Name: netname}
err := network.Get(r.Context())
if err != nil {
logger.Log(0, r.Header.Get("user"), fmt.Sprintf("failed to fetch network [%s] info: %v",
netname, err))
errType := logic.Internal
if errors.Is(err, gorm.ErrRecordNotFound) {
errType = logic.NotFound
}
logic.ReturnErrorResponse(w, r, logic.FormatError(err, errType))
return
}
logger.Log(2, r.Header.Get("user"), "fetched network", netname)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(network)
}
// @Summary Get a network Egress routes
// @Router /api/networks/{networkname}/egress_routes [get]
// @Tags Networks
// @Security oauth
// @Param networkname path string true "Network name"
// @Produce json
// @Success 200 {object} map[string][]string
// @Failure 500 {object} models.ErrorResponse
func getNetworkEgressRoutes(w http.ResponseWriter, r *http.Request) {
var params = mux.Vars(r)
netname := params["networkname"]
// check if network exists
err := (&schema.Network{Name: netname}).Get(r.Context())
if err != nil {
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to fetch ACLs for network [%s]: %v", netname, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
nodeEgressRoutes, _, err := logic.GetEgressRanges(r.Context(), schema.NetworkID(netname))
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
logic.ReturnSuccessResponseWithJson(w, r, nodeEgressRoutes, "fetched network egress routes")
}
// @Summary Delete a network
// @Router /api/networks/{networkname} [delete]
// @Tags Networks
// @Security oauth
// @Param networkname path string true "Network name"
// @Param force query bool false "Force Delete"
// @Produce json
// @Success 200 {object} models.SuccessResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 403 {object} models.ErrorResponse
func deleteNetwork(w http.ResponseWriter, r *http.Request) {
// Set header
w.Header().Set("Content-Type", "application/json")
username := r.Header.Get("user")
if username != logic.MasterUser {
user := &schema.User{Username: username}
if err := user.Get(r.Context()); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access denied"), logic.Forbidden))
return
}
if user.PlatformRoleID != schema.SuperAdminRole && user.PlatformRoleID != schema.AdminRole {
logic.ReturnErrorResponse(w, r, logic.FormatError(
errors.New("only platform admins can delete networks"), logic.Forbidden))
return
}
}
force := r.URL.Query().Get("force") == "true"
var params = mux.Vars(r)
network := params["networkname"]
doneCh := make(chan struct{}, 1)
networkNodes, err := logic.GetNetworkNodes(r.Context(), network)
if err != nil {
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to get network nodes [%s]: %v", network, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
err = logic.DeleteNetwork(r.Context(), network, force, doneCh)
if err != nil {
errtype := logic.BadReq
if strings.Contains(err.Error(), "Node check failed") {
errtype = logic.Forbidden
}
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to delete network [%s]: %v", network, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype))
return
}
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
go logic.UnlinkNetworkAndTagsFromEnrollmentKeys(ctx, network, true)
go logic.DeleteNetworkRoles(ctx, network)
go logic.DeleteAllNetworkTags(ctx, schema.NetworkID(network))
go logic.DeleteNetworkPolicies(ctx, schema.NetworkID(network))
go func(ctx context.Context) {
<-doneCh
mq.PublishPeerUpdate(ctx, true)
// send node update to clean up locally
for _, node := range networkNodes {
node := node
node.PendingDelete = true
node.Action = schema.NODE_DELETE
if err := mq.NodeUpdate(&node); err != nil {
slog.Error("error publishing node update to node", "node", node.ID, "error", err)
}
}
err = (&schema.Nameserver{}).Delete(db.WithContext(context.TODO()), dbtypes.WithFilter("network_id", network))
if err != nil {
slog.Error("error deleting network nameservers", "network", network, "error", err)
}
err = (&schema.Egress{}).Delete(db.WithContext(context.TODO()), dbtypes.WithFilter("network", network))
if err != nil {
slog.Error("error deleting network egress rules", "network", network, "error", err)
}
err = (&schema.PostureCheck{}).Delete(db.WithContext(context.TODO()), dbtypes.WithFilter("network_id", network))
if err != nil {
slog.Error("error deleting network posture checks", "network", network, "error", err)
}
}(ctx)
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: network,
Name: network,
Type: schema.NetworkSub,
},
Origin: schema.Dashboard,
Diff: models.Diff{
Old: network,
New: nil,
},
})
logger.Log(1, r.Header.Get("user"), "deleted network", network)
w.WriteHeader(http.StatusOK)
logic.ReturnSuccessResponse(w, r, "success")
}
// @Summary Create a network
// @Router /api/networks [post]
// @Tags Networks
// @Security oauth
// @Param body body schema.Network true "Network details"
// @Produce json
// @Success 200 {object} schema.Network
// @Failure 400 {object} models.ErrorResponse
func createNetwork(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var network schema.Network
// we decode our body request params
err := json.NewDecoder(r.Body).Decode(&network)
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
}
featureFlags := logic.GetFeatureFlags(r.Context())
if !featureFlags.EnableDeviceApproval {
network.AutoJoin = true
}
if len(network.Name) > 32 {
err := errors.New("network name shouldn't exceed 32 characters")
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
if network.AddressRange == "" && network.AddressRange6 == "" {
err := errors.New("IPv4 or IPv6 CIDR required")
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
// validate address ranges: must be private
if network.AddressRange != "" {
_, cidr, err := net.ParseCIDR(network.AddressRange)
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
} else {
ones, bits := cidr.Mask.Size()
if bits-ones <= 1 {
err = fmt.Errorf("cannot create network with /31 or /32 cidr")
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
}
if network.AddressRange6 != "" {
_, cidr, err := net.ParseCIDR(network.AddressRange6)
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
} else {
ones, bits := cidr.Mask.Size()
if bits-ones <= 1 {
err = fmt.Errorf("cannot create network with /127 or /128 cidr")
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
}
if network.AutoRemove {
if network.AutoRemoveThreshold == 0 {
network.AutoRemoveThreshold = 60
}
}
if network.AutoRemoveTags == nil {
network.AutoRemoveTags = []string{}
}
network.TenantID = scope.ID(r.Context())
err = logic.CreateNetwork(r.Context(), &network)
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
errType := logic.BadReq
if errors.Is(err, logic.ErrNetworkLimitExceeded) {
errType = logic.Forbidden
}
logic.ReturnErrorResponse(w, r, logic.FormatError(err, errType))
return
}
logic.CreateDefaultNetworkEnrollmentKey(r.Context(), network.Name)
logic.CreateDefaultNetworkRolesAndGroups(r.Context(), schema.NetworkID(network.Name), r.Header.Get("user"))
logic.CreateDefaultAclNetworkPolicies(r.Context(), schema.NetworkID(network.Name))
logic.CreateDefaultTags(r.Context(), schema.NetworkID(network.Name))
logic.CreateFallbackNameserver(&network)
if featureFlags.EnableOverlappingEgressRanges {
if err := logic.AllocateUniqueVNATPool(r.Context(), &network); err != nil {
logger.Log(0, r.Header.Get("user"), "failed to allocate unique virtual NAT pool:", err.Error())
} else if err := network.Update(r.Context()); err != nil {
logger.Log(0, r.Header.Get("user"), "failed to update network with virtual NAT settings:", err.Error())
}
}
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
go func(ctx context.Context) {
defaultHosts := logic.GetDefaultHosts(ctx)
for i := range defaultHosts {
host := &defaultHosts[i]
newNode, err := orchestrator.GetRepository().NodeOrchestrator().CreateNode(ctx, host, &network)
if err != nil {
logger.Log(
0,
r.Header.Get("user"),
"failed to add host to network:",
host.ID.String(),
network.Name,
err.Error(),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logger.Log(1, "added new node", newNode.ID, "to host", host.Name)
}
}(ctx)
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: network.Name,
Name: network.Name,
Type: schema.NetworkSub,
Info: network,
},
Origin: schema.Dashboard,
})
logger.Log(1, r.Header.Get("user"), "created network", network.Name)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(network)
}
// @Summary Update network settings
// @Router /api/networks/{networkname} [put]
// @Tags Networks
// @Security oauth
// @Param networkname path string true "Network name"
// @Param body body schema.Network true "Network details"
// @Produce json
// @Success 200 {object} schema.Network
// @Failure 400 {object} models.ErrorResponse
func updateNetwork(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var payload schema.Network
// we decode our body request params
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
slog.Info("error decoding request body", "user", r.Header.Get("user"), "err", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
currNet := &schema.Network{Name: payload.Name}
err = currNet.Get(r.Context())
if err != nil {
slog.Info("error fetching network", "user", r.Header.Get("user"), "err", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = logic.UpdateNetwork(r.Context(), currNet, &payload)
if err != nil {
slog.Info("failed to update network", "user", r.Header.Get("user"), "err", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
go mq.PublishPeerUpdate(ctx, false)
slog.Info("updated network", "network", payload.Name, "user", r.Header.Get("user"))
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(currNet)
}