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

276 lines
8.5 KiB
Go

package controllers
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
ch "github.com/gravitl/netmaker/clickhouse"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/middleware"
"github.com/gravitl/netmaker/scope"
"gorm.io/gorm"
)
func FlowHandlers(r *mux.Router) {
r.HandleFunc("/api/v1/flows", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(handleListFlows)))).Methods(http.MethodGet)
}
const (
querySelect = `
SELECT
flow_id, tenant_id, host_id, host_name, network_id,
protocol, src_port, dst_port,
icmp_type, icmp_code, direction,
src_ip, src_type, src_entity_id, src_entity_name,
dst_ip, dst_type, dst_entity_id, dst_entity_name,
start_ts, end_ts,
bytes_sent, bytes_recv,
packets_sent, packets_recv,
status, version
FROM flows`
queryOrder = `
ORDER BY version DESC
LIMIT ? OFFSET ?`
)
// FlowRow represents a single flow log entry
type FlowRow struct {
FlowID string `ch:"flow_id" json:"flow_id"`
TenantID string `ch:"tenant_id" json:"tenant_id"`
HostID string `ch:"host_id" json:"host_id"`
HostName string `ch:"host_name" json:"host_name"`
NetworkID string `ch:"network_id" json:"network_id"`
Protocol uint16 `ch:"protocol" json:"protocol"`
SrcPort uint16 `ch:"src_port" json:"src_port"`
DstPort uint16 `ch:"dst_port" json:"dst_port"`
ICMPType uint8 `ch:"icmp_type" json:"icmp_type"`
ICMPCode uint8 `ch:"icmp_code" json:"icmp_code"`
Direction string `ch:"direction" json:"direction"`
SrcIP string `ch:"src_ip" json:"src_ip"`
SrcType string `ch:"src_type" json:"src_type"`
SrcEntityID string `ch:"src_entity_id" json:"src_entity_id"`
SrcEntityName string `ch:"src_entity_name" json:"src_entity_name"`
DstIP string `ch:"dst_ip" json:"dst_ip"`
DstType string `ch:"dst_type" json:"dst_type"`
DstEntityID string `ch:"dst_entity_id" json:"dst_entity_id"`
DstEntityName string `ch:"dst_entity_name" json:"dst_entity_name"`
StartTs time.Time `ch:"start_ts" json:"start_ts"`
EndTs time.Time `ch:"end_ts" json:"end_ts"`
BytesSent uint64 `ch:"bytes_sent" json:"bytes_sent"`
BytesRecv uint64 `ch:"bytes_recv" json:"bytes_recv"`
PacketsSent uint64 `ch:"packets_sent" json:"packets_sent"`
PacketsRecv uint64 `ch:"packets_recv" json:"packets_recv"`
Status uint32 `ch:"status" json:"status"`
Version time.Time `ch:"version" json:"version"`
}
// @Summary List flow logs
// @Router /api/v1/flows [get]
// @Tags Traffic Logs
// @Security oauth
// @Produce json
// @Param network_id query string false "Filter by network ID"
// @Param from query string false "Start time in RFC3339 format"
// @Param to query string false "End time in RFC3339 format"
// @Param src_type []query string false "Source type filter"
// @Param src_entity_id query string false "Source entity ID filter"
// @Param dst_type []query string false "Destination type filter"
// @Param dst_entity_id query string false "Destination entity ID filter"
// @Param protocol []query string false "Protocol filter"
// @Param node_id query string false "Node ID filter"
// @Param username query string false "Username filter"
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page (max 1000)"
// @Success 200 {array} FlowRow
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func handleListFlows(w http.ResponseWriter, r *http.Request) {
if !logic.GetFeatureFlags(r.Context()).EnableFlowLogs || !logic.GetServerSettings(r.Context()).EnableFlowLogs {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("flow logs not enabled"), logic.Forbidden))
return
}
conn, err := ch.FromContext(r.Context())
if err != nil {
logic.ReturnErrorResponse(w, r,
logic.FormatError(fmt.Errorf("clickhouse connection not available: %v", err), logic.Internal))
return
}
q := r.URL.Query()
// TODO: handle query filters better
var (
whereParts []string
args []any
)
// -1. Tenant filter.
whereParts = append(whereParts, "tenant_id = ?")
args = append(args, scope.ID(r.Context()))
// 0. Network filter.
networkID := q.Get("network_id")
if networkID != "" {
whereParts = append(whereParts, "network_id = ?")
args = append(args, networkID)
}
// 1. Time filtering (start_ts: UInt64 timestamp in ms)
fromStr := q.Get("from")
toStr := q.Get("to")
if fromStr != "" {
fromVal, err := time.Parse(time.RFC3339, fromStr)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid 'from' timestamp: %v", err), logic.BadReq))
return
}
whereParts = append(whereParts, "start_ts >= ?")
args = append(args, fromVal)
}
if toStr != "" {
toVal, err := time.Parse(time.RFC3339, toStr)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid 'to' timestamp: %v", err), logic.BadReq))
return
}
whereParts = append(whereParts, "start_ts <= ?")
args = append(args, toVal)
}
// 2. Source filters
if q.Get("src_type") != "" {
whereParts = append(whereParts, "src_type IN ?")
args = append(args, q["src_type"])
}
srcEntity := q.Get("src_entity_id")
if srcEntity != "" {
whereParts = append(whereParts, "src_entity_id = ?")
args = append(args, srcEntity)
}
// 3. Destination filters
if q.Get("dst_type") != "" {
whereParts = append(whereParts, "dst_type IN ?")
args = append(args, q["dst_type"])
}
dstEntity := q.Get("dst_entity_id")
if dstEntity != "" {
whereParts = append(whereParts, "dst_entity_id = ?")
args = append(args, dstEntity)
}
// 4. Protocol filter
if q.Get("protocol") != "" {
whereParts = append(whereParts, "protocol IN ?")
args = append(args, q["protocol"])
}
// 5. Node filter
nodeID := q.Get("node_id")
if nodeID != "" {
node, err := logic.GetNodeByID(nodeID)
if err != nil {
errType := logic.Internal
if errors.Is(err, gorm.ErrRecordNotFound) {
errType = logic.BadReq
}
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("error fetching node with id %s: %v", nodeID, err), errType))
return
}
if networkID == "" {
whereParts = append(whereParts, "network_id = ?")
args = append(args, node.Network)
} else {
if networkID != node.Network {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("node with id %s does not belong to network %s", nodeID, networkID), logic.BadReq))
return
}
}
whereParts = append(whereParts, "host_id = ?")
args = append(args, node.HostID)
}
// 6. User filter
username := q.Get("username")
if username != "" {
if q.Has("src_type") || q.Has("dst_type") ||
q.Has("src_entity_id") || q.Has("dst_entity_id") {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot provide username filter along with src/dst type and id filters"), logic.BadReq))
return
}
srcTypeStr := "user"
srcEntity := username
dstTypeStr := "user"
dstEntity := username
whereParts = append(whereParts, "((src_type = ? AND src_entity_id = ?) OR (dst_type = ? AND dst_entity_id = ?))")
args = append(args, srcTypeStr, srcEntity, dstTypeStr, dstEntity)
}
// 7. Ignore flow logs with zero end_ts.
whereParts = append(whereParts, "end_ts <> ?")
args = append(args, time.Unix(0, 0))
// Pagination
page := parseIntOrDefault(q.Get("page"), 1)
perPage := parseIntOrDefault(q.Get("per_page"), 100)
if perPage > 1000 {
perPage = 1000
}
offset := (page - 1) * perPage
whereSQL := ""
if len(whereParts) > 0 {
whereSQL = "WHERE " + strings.Join(whereParts, " AND ")
}
query := querySelect + "\n" + whereSQL + "\n" + queryOrder
args = append(args, perPage, offset)
rows, err := conn.Query(r.Context(), query, args...)
if err != nil {
logic.ReturnErrorResponse(w, r,
logic.FormatError(fmt.Errorf("error fetching flows: %v", err), logic.Internal))
return
}
defer rows.Close()
result := make([]FlowRow, 0, 1000)
for rows.Next() {
var fr FlowRow
if err := rows.ScanStruct(&fr); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("error fetching flows: %v", err), logic.Internal))
return
}
result = append(result, fr)
}
logic.ReturnSuccessResponseWithJson(w, r, result, "flows retrieved successfully")
}
func parseIntOrDefault(s string, def int) int {
if s == "" {
return def
}
v, err := strconv.Atoi(s)
if err != nil || v <= 0 {
return def
}
return v
}