netbird/management/server/http/rules.go
Misha Bragin 5c0b8a46f0
Add system activity tracking and event store (#636)
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.
2023-01-02 15:11:32 +01:00

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
}