mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-23 08:33:45 +01:00
5c0b8a46f0
This PR adds system activity tracking. The management service records events like add/remove peer, group, rule, route, etc. The activity events are stored in the SQLite event store and can be queried by the HTTP API.
433 lines
12 KiB
Go
433 lines
12 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"github.com/gorilla/mux"
|
|
"github.com/netbirdio/netbird/management/server"
|
|
"github.com/netbirdio/netbird/management/server/http/api"
|
|
"github.com/netbirdio/netbird/management/server/http/util"
|
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
|
"github.com/netbirdio/netbird/management/server/status"
|
|
"github.com/rs/xid"
|
|
"net/http"
|
|
)
|
|
|
|
// Rules is a handler that returns rules of the account
|
|
type Rules struct {
|
|
jwtExtractor jwtclaims.ClaimsExtractor
|
|
accountManager server.AccountManager
|
|
authAudience string
|
|
}
|
|
|
|
func NewRules(accountManager server.AccountManager, authAudience string) *Rules {
|
|
return &Rules{
|
|
accountManager: accountManager,
|
|
authAudience: authAudience,
|
|
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
|
}
|
|
}
|
|
|
|
// GetAllRulesHandler list for the account
|
|
func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
|
|
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
|
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
accountRules, err := h.accountManager.ListRules(account.Id, user.Id)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
rules := []*api.Rule{}
|
|
for _, r := range accountRules {
|
|
rules = append(rules, toRuleResponse(account, r))
|
|
}
|
|
|
|
util.WriteJSONObject(w, rules)
|
|
}
|
|
|
|
// UpdateRuleHandler handles update to a rule identified by a given ID
|
|
func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
|
|
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
|
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
ruleID := vars["id"]
|
|
if len(ruleID) == 0 {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
|
|
return
|
|
}
|
|
|
|
_, ok := account.Rules[ruleID]
|
|
if !ok {
|
|
util.WriteError(status.Errorf(status.NotFound, "couldn't find rule id %s", ruleID), w)
|
|
return
|
|
}
|
|
|
|
var req api.PutApiRulesIdJSONRequestBody
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
|
}
|
|
|
|
if req.Name == "" {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w)
|
|
return
|
|
}
|
|
|
|
var reqSources []string
|
|
if req.Sources != nil {
|
|
reqSources = *req.Sources
|
|
}
|
|
|
|
var reqDestinations []string
|
|
if req.Destinations != nil {
|
|
reqDestinations = *req.Destinations
|
|
}
|
|
|
|
rule := server.Rule{
|
|
ID: ruleID,
|
|
Name: req.Name,
|
|
Source: reqSources,
|
|
Destination: reqDestinations,
|
|
Disabled: req.Disabled,
|
|
Description: req.Description,
|
|
}
|
|
|
|
switch req.Flow {
|
|
case server.TrafficFlowBidirectString:
|
|
rule.Flow = server.TrafficFlowBidirect
|
|
default:
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "unknown flow type"), w)
|
|
return
|
|
}
|
|
|
|
err = h.accountManager.SaveRule(account.Id, user.Id, &rule)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
resp := toRuleResponse(account, &rule)
|
|
|
|
util.WriteJSONObject(w, &resp)
|
|
}
|
|
|
|
// PatchRuleHandler handles patch updates to a rule identified by a given ID
|
|
func (h *Rules) PatchRuleHandler(w http.ResponseWriter, r *http.Request) {
|
|
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
|
account, _, err := h.accountManager.GetAccountFromToken(claims)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
ruleID := vars["id"]
|
|
if len(ruleID) == 0 {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
|
|
return
|
|
}
|
|
|
|
_, ok := account.Rules[ruleID]
|
|
if !ok {
|
|
util.WriteError(status.Errorf(status.NotFound, "couldn't find rule ID %s", ruleID), w)
|
|
return
|
|
}
|
|
|
|
var req api.PatchApiRulesIdJSONRequestBody
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
|
return
|
|
}
|
|
|
|
if len(req) == 0 {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "no patch instruction received"), w)
|
|
return
|
|
}
|
|
|
|
var operations []server.RuleUpdateOperation
|
|
|
|
for _, patch := range req {
|
|
switch patch.Path {
|
|
case api.RulePatchOperationPathName:
|
|
if patch.Op != api.RulePatchOperationOpReplace {
|
|
util.WriteError(status.Errorf(status.InvalidArgument,
|
|
"name field only accepts replace operation, got %s", patch.Op), w)
|
|
return
|
|
}
|
|
if len(patch.Value) == 0 || patch.Value[0] == "" {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w)
|
|
return
|
|
}
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.UpdateRuleName,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationPathDescription:
|
|
if patch.Op != api.RulePatchOperationOpReplace {
|
|
util.WriteError(status.Errorf(status.InvalidArgument,
|
|
"description field only accepts replace operation, got %s", patch.Op), w)
|
|
return
|
|
}
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.UpdateRuleDescription,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationPathFlow:
|
|
if patch.Op != api.RulePatchOperationOpReplace {
|
|
util.WriteError(status.Errorf(status.InvalidArgument,
|
|
"flow field only accepts replace operation, got %s", patch.Op), w)
|
|
return
|
|
}
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.UpdateRuleFlow,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationPathDisabled:
|
|
if patch.Op != api.RulePatchOperationOpReplace {
|
|
util.WriteError(status.Errorf(status.InvalidArgument,
|
|
"disabled field only accepts replace operation, got %s", patch.Op), w)
|
|
return
|
|
}
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.UpdateRuleStatus,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationPathSources:
|
|
switch patch.Op {
|
|
case api.RulePatchOperationOpReplace:
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.UpdateSourceGroups,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationOpRemove:
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.RemoveGroupsFromSource,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationOpAdd:
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.InsertGroupsToSource,
|
|
Values: patch.Value,
|
|
})
|
|
default:
|
|
util.WriteError(status.Errorf(status.InvalidArgument,
|
|
"invalid operation \"%s\" on Source field", patch.Op), w)
|
|
return
|
|
}
|
|
case api.RulePatchOperationPathDestinations:
|
|
switch patch.Op {
|
|
case api.RulePatchOperationOpReplace:
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.UpdateDestinationGroups,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationOpRemove:
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.RemoveGroupsFromDestination,
|
|
Values: patch.Value,
|
|
})
|
|
case api.RulePatchOperationOpAdd:
|
|
operations = append(operations, server.RuleUpdateOperation{
|
|
Type: server.InsertGroupsToDestination,
|
|
Values: patch.Value,
|
|
})
|
|
default:
|
|
util.WriteError(status.Errorf(status.InvalidArgument,
|
|
"invalid operation \"%s\" on Destination field", patch.Op), w)
|
|
return
|
|
}
|
|
default:
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "invalid patch path"), w)
|
|
return
|
|
}
|
|
}
|
|
|
|
rule, err := h.accountManager.UpdateRule(account.Id, ruleID, operations)
|
|
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
resp := toRuleResponse(account, rule)
|
|
|
|
util.WriteJSONObject(w, &resp)
|
|
}
|
|
|
|
// CreateRuleHandler handles rule creation request
|
|
func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
|
|
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
|
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
var req api.PostApiRulesJSONRequestBody
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
|
return
|
|
}
|
|
|
|
if req.Name == "" {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w)
|
|
return
|
|
}
|
|
|
|
var reqSources []string
|
|
if req.Sources != nil {
|
|
reqSources = *req.Sources
|
|
}
|
|
|
|
var reqDestinations []string
|
|
if req.Destinations != nil {
|
|
reqDestinations = *req.Destinations
|
|
}
|
|
|
|
rule := server.Rule{
|
|
ID: xid.New().String(),
|
|
Name: req.Name,
|
|
Source: reqSources,
|
|
Destination: reqDestinations,
|
|
Disabled: req.Disabled,
|
|
Description: req.Description,
|
|
}
|
|
|
|
switch req.Flow {
|
|
case server.TrafficFlowBidirectString:
|
|
rule.Flow = server.TrafficFlowBidirect
|
|
default:
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "unknown flow type"), w)
|
|
return
|
|
}
|
|
|
|
err = h.accountManager.SaveRule(account.Id, user.Id, &rule)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
resp := toRuleResponse(account, &rule)
|
|
|
|
util.WriteJSONObject(w, &resp)
|
|
}
|
|
|
|
// DeleteRuleHandler handles rule deletion request
|
|
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
|
|
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
|
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
aID := account.Id
|
|
|
|
rID := mux.Vars(r)["id"]
|
|
if len(rID) == 0 {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
|
|
return
|
|
}
|
|
|
|
err = h.accountManager.DeleteRule(aID, rID, user.Id)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
util.WriteJSONObject(w, "")
|
|
}
|
|
|
|
// GetRuleHandler handles a group Get request identified by ID
|
|
func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
|
|
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
|
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
|
if err != nil {
|
|
util.WriteError(err, w)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
ruleID := mux.Vars(r)["id"]
|
|
if len(ruleID) == 0 {
|
|
util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
|
|
return
|
|
}
|
|
|
|
rule, err := h.accountManager.GetRule(account.Id, ruleID, user.Id)
|
|
if err != nil {
|
|
util.WriteError(status.Errorf(status.NotFound, "rule not found"), w)
|
|
return
|
|
}
|
|
|
|
util.WriteJSONObject(w, toRuleResponse(account, rule))
|
|
default:
|
|
util.WriteError(status.Errorf(status.NotFound, "method not found"), w)
|
|
}
|
|
}
|
|
|
|
func toRuleResponse(account *server.Account, rule *server.Rule) *api.Rule {
|
|
cache := make(map[string]api.GroupMinimum)
|
|
gr := api.Rule{
|
|
Id: rule.ID,
|
|
Name: rule.Name,
|
|
Description: rule.Description,
|
|
Disabled: rule.Disabled,
|
|
}
|
|
|
|
switch rule.Flow {
|
|
case server.TrafficFlowBidirect:
|
|
gr.Flow = server.TrafficFlowBidirectString
|
|
default:
|
|
gr.Flow = "unknown"
|
|
}
|
|
|
|
for _, gid := range rule.Source {
|
|
_, ok := cache[gid]
|
|
if ok {
|
|
continue
|
|
}
|
|
|
|
if group, ok := account.Groups[gid]; ok {
|
|
minimum := api.GroupMinimum{
|
|
Id: group.ID,
|
|
Name: group.Name,
|
|
PeersCount: len(group.Peers),
|
|
}
|
|
|
|
gr.Sources = append(gr.Sources, minimum)
|
|
cache[gid] = minimum
|
|
}
|
|
}
|
|
|
|
for _, gid := range rule.Destination {
|
|
cachedMinimum, ok := cache[gid]
|
|
if ok {
|
|
gr.Destinations = append(gr.Destinations, cachedMinimum)
|
|
continue
|
|
}
|
|
if group, ok := account.Groups[gid]; ok {
|
|
minimum := api.GroupMinimum{
|
|
Id: group.ID,
|
|
Name: group.Name,
|
|
PeersCount: len(group.Peers),
|
|
}
|
|
gr.Destinations = append(gr.Destinations, minimum)
|
|
cache[gid] = minimum
|
|
}
|
|
}
|
|
|
|
return &gr
|
|
}
|