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

249 lines
7.4 KiB
Go

package logic
import (
"context"
"errors"
"sync"
"time"
"github.com/google/uuid"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/schema"
"github.com/gravitl/netmaker/scope"
"gorm.io/gorm"
)
const (
// ZOMBIE_TIMEOUT - timeout in hours for checking zombie status
ZOMBIE_TIMEOUT = 6
// ZOMBIE_DELETE_TIME - timeout in minutes for zombie node deletion
ZOMBIE_DELETE_TIME = 10
)
var (
// zombieChans and hostZombieChans map tenant ID -> that tenant's zombie-candidate channel.
zombieChans sync.Map
hostZombieChans sync.Map
)
func zombieChan(tenantID string) chan uuid.UUID {
ch, _ := zombieChans.LoadOrStore(tenantID, make(chan uuid.UUID, 10))
return ch.(chan uuid.UUID)
}
func hostZombieChan(tenantID string) chan uuid.UUID {
ch, _ := hostZombieChans.LoadOrStore(tenantID, make(chan uuid.UUID, 10))
return ch.(chan uuid.UUID)
}
// CheckZombies - checks if new node has same hostid as existing node
// if so, existing node is added to zombie node quarantine list
// also cleans up nodes past their expiration date
func CheckZombies(ctx context.Context, _node *schema.Node) {
nodes, err := GetNetworkNodes(ctx, _node.Network.Name)
if err != nil {
logger.Log(1, "Failed to retrieve network nodes", _node.Network.Name, err.Error())
return
}
zombies := zombieChan(scope.ID(ctx))
for _, node := range nodes {
if node.ID.String() == _node.ID {
//skip self
continue
}
if node.HostID.String() == _node.HostID {
logger.Log(0, "adding ", node.ID.String(), " to zombie list")
zombies <- node.ID
}
}
}
// checkForZombieHosts - checks if new host has the same macAddress as an existing host
// if true, existing host is added to host zombie collection
func checkForZombieHosts(ctx context.Context, h *schema.Host) {
hosts, err := (&schema.Host{}).ListAll(db.WithContext(ctx))
if err != nil {
logger.Log(3, "error retrieving all hosts", err.Error())
}
hostZombies := hostZombieChan(scope.ID(ctx))
for _, existing := range hosts {
if existing.ID == h.ID {
//probably an unnecessary check as new host should not be in database yet, but just in case
//skip self
continue
}
if existing.MacAddress.String() == h.MacAddress.String() {
//add to hostZombies
hostZombies <- existing.ID
//add all nodes belonging to host to zombile list
for _, node := range existing.Nodes {
id, err := uuid.Parse(node)
if err != nil {
logger.Log(3, "error parsing uuid from host.Nodes", err.Error())
continue
}
hostZombies <- id
}
}
}
}
// ManageZombies - lists tenants and starts a per-tenant zombie-management worker for each.
func ManageZombies(ctx context.Context) {
logger.Log(2, "Zombie management started")
tenants, err := (&schema.Tenant{}).List(db.WithContext(ctx))
if err != nil {
logger.Log(1, "failed to list tenants for zombie management", err.Error())
}
for _, tenant := range tenants {
tenantCtx := scope.WithContext(db.WithContext(ctx), scope.TenantScope, tenant.ID)
go manageTenantZombies(tenantCtx)
}
<-ctx.Done()
close(DeleteNodesCh)
}
// manageTenantZombies - goroutine which adds/removes/deletes nodes from a single tenant's
// zombie node quarantine list.
func manageTenantZombies(ctx context.Context) {
tenantID := scope.ID(ctx)
zombieCh := zombieChan(tenantID)
hostZombieCh := hostZombieChan(tenantID)
var zombies, hostZombies []uuid.UUID
go initializeZombies(ctx)
go checkPendingRemovalNodes(ctx)
// Zombie Nodes Cleanup Four Times a Day
ticker := time.NewTicker(time.Hour * ZOMBIE_TIMEOUT)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case id := <-zombieCh:
zombies = append(zombies, id)
case id := <-hostZombieCh:
hostZombies = append(hostZombies, id)
case <-ticker.C: // run this check 4 times a day
logger.Log(3, "checking for zombie nodes", "tenant", tenantID)
cleanupOrphanedNodes(ctx)
if len(zombies) > 0 {
for i := len(zombies) - 1; i >= 0; i-- {
node, err := GetNodeByID(zombies[i].String())
if err != nil {
logger.Log(1, "error retrieving zombie node", zombies[i].String(), err.Error())
logger.Log(1, "deleting ", node.ID.String(), " from zombie list")
zombies = append(zombies[:i], zombies[i+1:]...)
continue
}
if time.Since(node.LastCheckIn) > time.Minute*ZOMBIE_DELETE_TIME {
if err := DeleteNode(ctx, &node, true); err != nil {
logger.Log(1, "error deleting zombie node", zombies[i].String(), err.Error())
continue
}
node.PendingDelete = true
node.Action = schema.NODE_DELETE
DeleteNodesCh <- &node
logger.Log(1, "deleting zombie node", node.ID.String())
zombies = append(zombies[:i], zombies[i+1:]...)
}
}
}
if len(hostZombies) > 0 {
logger.Log(3, "checking host zombies")
for i := len(hostZombies) - 1; i >= 0; i-- {
host := &schema.Host{ID: hostZombies[i]}
err := host.Get(ctx)
if err != nil {
logger.Log(1, "error retrieving zombie host", err.Error())
logger.Log(1, "deleting ", host.ID.String(), " from zombie list")
hostZombies = append(hostZombies[:i], hostZombies[i+1:]...)
continue
}
if len(host.Nodes) == 0 {
if err := RemoveHost(ctx, host, true); err != nil {
logger.Log(0, "error deleting zombie host", host.ID.String(), err.Error())
}
hostZombies = append(hostZombies[:i], hostZombies[i+1:]...)
}
}
}
}
}
}
// cleanupOrphanedNodes removes nodes whose host_id references a missing host record.
func cleanupOrphanedNodes(ctx context.Context) {
nodes, err := GetAllNodes(ctx)
if err != nil {
logger.Log(1, "failed to retrieve nodes for orphan cleanup", err.Error())
return
}
for _, n := range nodes {
node := n
if node.PendingDelete || node.Action == schema.NODE_DELETE {
continue
}
host := &schema.Host{ID: node.HostID}
if err := host.Get(ctx); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
logger.Log(0, "error checking host for orphaned node", node.ID.String(), err.Error())
continue
}
logger.Log(0, "orphaned node found (no host record), deleting", node.ID.String())
if err := DeleteNode(ctx, &node, true); err != nil {
logger.Log(0, "error deleting orphaned node", node.ID.String(), err.Error())
continue
}
}
}
}
func checkPendingRemovalNodes(ctx context.Context) {
nodes, _ := GetAllNodes(ctx)
for _, node := range nodes {
node := node
pendingDelete := node.PendingDelete || node.Action == schema.NODE_DELETE
if pendingDelete {
DeleteNode(ctx, &node, true)
DeleteNodesCh <- &node
continue
}
}
}
// initializeZombies - populates the zombie quarantine list (should be called from initialization)
func initializeZombies(ctx context.Context) {
nodes, err := GetAllNodes(ctx)
if err != nil {
logger.Log(1, "failed to retrieve nodes", err.Error())
return
}
zombies := zombieChan(scope.ID(ctx))
for _, node := range nodes {
othernodes, err := GetNetworkNodes(ctx, node.Network)
if err != nil {
logger.Log(1, "failled to retrieve nodes for network", node.Network, err.Error())
continue
}
for _, othernode := range othernodes {
if node.ID == othernode.ID {
continue
}
if node.HostID == othernode.HostID {
if node.LastCheckIn.After(othernode.LastCheckIn) {
zombies <- othernode.ID
logger.Log(1, "adding", othernode.ID.String(), "to zombie list")
} else {
zombies <- node.ID
logger.Log(1, "adding", node.ID.String(), "to zombie list")
}
}
}
}
cleanupOrphanedNodes(ctx)
}