mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-23 16:43:29 +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.
318 lines
8.6 KiB
Go
318 lines
8.6 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/netbirdio/netbird/management/server/http/api"
|
|
"github.com/netbirdio/netbird/management/server/status"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
|
|
|
"github.com/magiconair/properties/assert"
|
|
"github.com/netbirdio/netbird/management/server"
|
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
|
)
|
|
|
|
var TestPeers = map[string]*server.Peer{
|
|
"A": {Key: "A", IP: net.ParseIP("100.100.100.100")},
|
|
"B": {Key: "B", IP: net.ParseIP("200.200.200.200")},
|
|
}
|
|
|
|
func initGroupTestData(user *server.User, groups ...*server.Group) *Groups {
|
|
return &Groups{
|
|
accountManager: &mock_server.MockAccountManager{
|
|
SaveGroupFunc: func(accountID, userID string, group *server.Group) error {
|
|
if !strings.HasPrefix(group.ID, "id-") {
|
|
group.ID = "id-was-set"
|
|
}
|
|
return nil
|
|
},
|
|
GetGroupFunc: func(_, groupID string) (*server.Group, error) {
|
|
if groupID != "idofthegroup" {
|
|
return nil, status.Errorf(status.NotFound, "not found")
|
|
}
|
|
return &server.Group{
|
|
ID: "idofthegroup",
|
|
Name: "Group",
|
|
}, nil
|
|
},
|
|
UpdateGroupFunc: func(_ string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) {
|
|
var group server.Group
|
|
group.ID = groupID
|
|
for _, operation := range operations {
|
|
switch operation.Type {
|
|
case server.UpdateGroupName:
|
|
group.Name = operation.Values[0]
|
|
case server.UpdateGroupPeers, server.InsertPeersToGroup:
|
|
group.Peers = operation.Values
|
|
case server.RemovePeersFromGroup:
|
|
default:
|
|
return nil, fmt.Errorf("no operation")
|
|
}
|
|
}
|
|
return &group, nil
|
|
},
|
|
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
|
for _, peer := range TestPeers {
|
|
if peer.IP.String() == peerIP {
|
|
return peer, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("peer not found")
|
|
},
|
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
|
return &server.Account{
|
|
Id: claims.AccountId,
|
|
Domain: "hotmail.com",
|
|
Peers: TestPeers,
|
|
Users: map[string]*server.User{
|
|
user.Id: user,
|
|
},
|
|
Groups: map[string]*server.Group{
|
|
"id-existed": {ID: "id-existed", Peers: []string{"A", "B"}},
|
|
"id-all": {ID: "id-all", Name: "All"}},
|
|
}, user, nil
|
|
},
|
|
},
|
|
authAudience: "",
|
|
jwtExtractor: jwtclaims.ClaimsExtractor{
|
|
ExtractClaimsFromRequestContext: func(r *http.Request, authAudiance string) jwtclaims.AuthorizationClaims {
|
|
return jwtclaims.AuthorizationClaims{
|
|
UserId: "test_user",
|
|
Domain: "hotmail.com",
|
|
AccountId: "test_id",
|
|
}
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGetGroup(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
expectedStatus int
|
|
expectedBody bool
|
|
requestType string
|
|
requestPath string
|
|
requestBody io.Reader
|
|
}{
|
|
{
|
|
name: "GetGroup OK",
|
|
expectedBody: true,
|
|
requestType: http.MethodGet,
|
|
requestPath: "/api/groups/idofthegroup",
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "GetGroup not found",
|
|
requestType: http.MethodGet,
|
|
requestPath: "/api/groups/notexists",
|
|
expectedStatus: http.StatusNotFound,
|
|
},
|
|
}
|
|
|
|
group := &server.Group{
|
|
ID: "idofthegroup",
|
|
Name: "Group",
|
|
}
|
|
|
|
adminUser := server.NewAdminUser("test_user")
|
|
p := initGroupTestData(adminUser, group)
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
|
|
|
router := mux.NewRouter()
|
|
router.HandleFunc("/api/groups/{id}", p.GetGroupHandler).Methods("GET")
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
res := recorder.Result()
|
|
defer res.Body.Close()
|
|
|
|
if status := recorder.Code; status != tc.expectedStatus {
|
|
t.Errorf("handler returned wrong status code: got %v want %v",
|
|
status, tc.expectedStatus)
|
|
return
|
|
}
|
|
|
|
if !tc.expectedBody {
|
|
return
|
|
}
|
|
|
|
content, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatalf("I don't know what I expected; %v", err)
|
|
}
|
|
|
|
got := &server.Group{}
|
|
if err = json.Unmarshal(content, &got); err != nil {
|
|
t.Fatalf("Sent content is not in correct json format; %v", err)
|
|
}
|
|
|
|
assert.Equal(t, got.ID, group.ID)
|
|
assert.Equal(t, got.Name, group.Name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteGroup(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
expectedStatus int
|
|
expectedBody bool
|
|
expectedGroup *api.Group
|
|
requestType string
|
|
requestPath string
|
|
requestBody io.Reader
|
|
}{
|
|
{
|
|
name: "Write Group POST OK",
|
|
requestType: http.MethodPost,
|
|
requestPath: "/api/groups",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`{"Name":"Default POSTed Group"}`)),
|
|
expectedStatus: http.StatusOK,
|
|
expectedBody: true,
|
|
expectedGroup: &api.Group{
|
|
Id: "id-was-set",
|
|
Name: "Default POSTed Group",
|
|
},
|
|
},
|
|
{
|
|
name: "Write Group POST Invalid Name",
|
|
requestType: http.MethodPost,
|
|
requestPath: "/api/groups",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`{"name":""}`)),
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
expectedBody: false,
|
|
},
|
|
{
|
|
name: "Write Group PUT OK",
|
|
requestType: http.MethodPut,
|
|
requestPath: "/api/groups/id-existed",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`{"Name":"Default POSTed Group"}`)),
|
|
expectedStatus: http.StatusOK,
|
|
expectedGroup: &api.Group{
|
|
Id: "id-existed",
|
|
Name: "Default POSTed Group",
|
|
},
|
|
},
|
|
{
|
|
name: "Write Group PUT Invalid Name",
|
|
requestType: http.MethodPut,
|
|
requestPath: "/api/groups/id-existed",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`{"Name":""}`)),
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
expectedBody: false,
|
|
},
|
|
{
|
|
name: "Write Group PUT All Group Name",
|
|
requestType: http.MethodPut,
|
|
requestPath: "/api/groups/id-all",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`{"Name":"super"}`)),
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
expectedBody: false,
|
|
},
|
|
{
|
|
name: "Write Group PATCH Name OK",
|
|
requestType: http.MethodPatch,
|
|
requestPath: "/api/groups/id-existed",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`[{"op":"replace","path":"name","value":["Default POSTed Group"]}]`)),
|
|
expectedStatus: http.StatusOK,
|
|
expectedGroup: &api.Group{
|
|
Id: "id-existed",
|
|
Name: "Default POSTed Group",
|
|
},
|
|
},
|
|
{
|
|
name: "Write Group PATCH Invalid Name OP",
|
|
requestType: http.MethodPatch,
|
|
requestPath: "/api/groups/id-existed",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`[{"op":"insert","path":"name","value":[""]}]`)),
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
expectedBody: false,
|
|
},
|
|
{
|
|
name: "Write Group PATCH Invalid Name",
|
|
requestType: http.MethodPatch,
|
|
requestPath: "/api/groups/id-existed",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`[{"op":"replace","path":"name","value":[]}]`)),
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
expectedBody: false,
|
|
},
|
|
{
|
|
name: "Write Group PATCH Peers OK",
|
|
requestType: http.MethodPatch,
|
|
requestPath: "/api/groups/id-existed",
|
|
requestBody: bytes.NewBuffer(
|
|
[]byte(`[{"op":"replace","path":"peers","value":["100.100.100.100","200.200.200.200"]}]`)),
|
|
expectedStatus: http.StatusOK,
|
|
expectedBody: true,
|
|
expectedGroup: &api.Group{
|
|
Id: "id-existed",
|
|
PeersCount: 2,
|
|
Peers: []api.PeerMinimum{
|
|
{Id: "100.100.100.100"},
|
|
{Id: "200.200.200.200"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
adminUser := server.NewAdminUser("test_user")
|
|
p := initGroupTestData(adminUser)
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
|
|
|
router := mux.NewRouter()
|
|
router.HandleFunc("/api/groups", p.CreateGroupHandler).Methods("POST")
|
|
router.HandleFunc("/api/groups/{id}", p.UpdateGroupHandler).Methods("PUT")
|
|
router.HandleFunc("/api/groups/{id}", p.PatchGroupHandler).Methods("PATCH")
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
res := recorder.Result()
|
|
defer res.Body.Close()
|
|
|
|
content, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatalf("I don't know what I expected; %v", err)
|
|
}
|
|
|
|
if status := recorder.Code; status != tc.expectedStatus {
|
|
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
|
|
status, tc.expectedStatus, string(content))
|
|
return
|
|
}
|
|
|
|
if !tc.expectedBody {
|
|
return
|
|
}
|
|
|
|
got := &api.Group{}
|
|
if err = json.Unmarshal(content, &got); err != nil {
|
|
t.Fatalf("Sent content is not in correct json format; %v", err)
|
|
}
|
|
assert.Equal(t, got, tc.expectedGroup)
|
|
})
|
|
}
|
|
}
|