package handlers import ( "encoding/json" "net/http" "strings" "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/shared/management/http/util" "github.com/netbirdio/netbird/shared/management/status" ) // minWindowSeconds is the floor enforced on enabled token / budget // limit windows. One minute is short enough for fine-grained burst // control without producing untenable consumption-row volume at scale. const minWindowSeconds int64 = 60 // addPolicyEndpoints registers all Agent Network policy routes on the // shared handler. func (h *handler) addPolicyEndpoints(router *mux.Router) { router.HandleFunc("/agent-network/policies", h.getAllPolicies).Methods("GET", "OPTIONS") router.HandleFunc("/agent-network/policies", h.createPolicy).Methods("POST", "OPTIONS") router.HandleFunc("/agent-network/policies/{policyId}", h.getPolicy).Methods("GET", "OPTIONS") router.HandleFunc("/agent-network/policies/{policyId}", h.updatePolicy).Methods("PUT", "OPTIONS") router.HandleFunc("/agent-network/policies/{policyId}", h.deletePolicy).Methods("DELETE", "OPTIONS") } func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } policies, err := h.manager.GetAllPolicies(r.Context(), userAuth.AccountId, userAuth.UserId) if err != nil { util.WriteError(r.Context(), err, w) return } out := make([]*api.AgentNetworkPolicy, 0, len(policies)) for _, p := range policies { out = append(out, p.ToAPIResponse()) } util.WriteJSONObject(r.Context(), w, out) } func (h *handler) getPolicy(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } policyID := mux.Vars(r)["policyId"] if policyID == "" { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "policy ID is required"), w) return } policy, err := h.manager.GetPolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policyID) if err != nil { util.WriteError(r.Context(), err, w) return } util.WriteJSONObject(r.Context(), w, policy.ToAPIResponse()) } func (h *handler) createPolicy(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } var req api.AgentNetworkPolicyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) return } if err := validatePolicy(&req); err != nil { util.WriteError(r.Context(), err, w) return } policy := types.NewPolicy(userAuth.AccountId) policy.FromAPIRequest(&req) created, err := h.manager.CreatePolicy(r.Context(), userAuth.UserId, policy) if err != nil { util.WriteError(r.Context(), err, w) return } util.WriteJSONObject(r.Context(), w, created.ToAPIResponse()) } func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } policyID := mux.Vars(r)["policyId"] if policyID == "" { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "policy ID is required"), w) return } var req api.AgentNetworkPolicyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) return } if err := validatePolicy(&req); err != nil { util.WriteError(r.Context(), err, w) return } policy := &types.Policy{ ID: policyID, AccountID: userAuth.AccountId, } policy.FromAPIRequest(&req) updated, err := h.manager.UpdatePolicy(r.Context(), userAuth.UserId, policy) if err != nil { util.WriteError(r.Context(), err, w) return } util.WriteJSONObject(r.Context(), w, updated.ToAPIResponse()) } func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } policyID := mux.Vars(r)["policyId"] if policyID == "" { util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "policy ID is required"), w) return } if err := h.manager.DeletePolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policyID); err != nil { util.WriteError(r.Context(), err, w) return } util.WriteJSONObject(r.Context(), w, util.EmptyObject{}) } func validatePolicy(req *api.AgentNetworkPolicyRequest) error { if strings.TrimSpace(req.Name) == "" { return status.Errorf(status.InvalidArgument, "name is required") } if len(req.SourceGroups) == 0 { return status.Errorf(status.InvalidArgument, "source_groups must contain at least one group id") } for _, id := range req.SourceGroups { if strings.TrimSpace(id) == "" { return status.Errorf(status.InvalidArgument, "source_groups must not contain empty entries") } } if len(req.DestinationProviderIds) == 0 { return status.Errorf(status.InvalidArgument, "destination_provider_ids must contain at least one provider id") } for _, id := range req.DestinationProviderIds { if strings.TrimSpace(id) == "" { return status.Errorf(status.InvalidArgument, "destination_provider_ids must not contain empty entries") } } if req.GuardrailIds != nil { for _, id := range *req.GuardrailIds { if strings.TrimSpace(id) == "" { return status.Errorf(status.InvalidArgument, "guardrail_ids must not contain empty entries") } } } if req.Limits != nil { if err := validatePolicyLimits(*req.Limits); err != nil { return err } } return nil } func validatePolicyLimits(l api.AgentNetworkPolicyLimits) error { if l.TokenLimit.Enabled { if l.TokenLimit.WindowSeconds < minWindowSeconds { return status.Errorf(status.InvalidArgument, "limits.token_limit.window_seconds must be at least %d (one minute) when enabled", minWindowSeconds) } if l.TokenLimit.GroupCap < 0 { return status.Errorf(status.InvalidArgument, "limits.token_limit.group_cap must not be negative") } if l.TokenLimit.UserCap < 0 { return status.Errorf(status.InvalidArgument, "limits.token_limit.user_cap must not be negative") } if l.TokenLimit.GroupCap == 0 && l.TokenLimit.UserCap == 0 { return status.Errorf(status.InvalidArgument, "limits.token_limit requires group_cap or user_cap to be greater than zero when enabled") } } if l.BudgetLimit.Enabled { if l.BudgetLimit.WindowSeconds < minWindowSeconds { return status.Errorf(status.InvalidArgument, "limits.budget_limit.window_seconds must be at least %d (one minute) when enabled", minWindowSeconds) } if l.BudgetLimit.GroupCapUsd < 0 { return status.Errorf(status.InvalidArgument, "limits.budget_limit.group_cap_usd must not be negative") } if l.BudgetLimit.UserCapUsd < 0 { return status.Errorf(status.InvalidArgument, "limits.budget_limit.user_cap_usd must not be negative") } if l.BudgetLimit.GroupCapUsd == 0 && l.BudgetLimit.UserCapUsd == 0 { return status.Errorf(status.InvalidArgument, "limits.budget_limit requires group_cap_usd or user_cap_usd to be greater than zero when enabled") } } return nil }