mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 00:06:38 +00:00
Add rules for ACL (#306)
Add rules HTTP endpoint for frontend - CRUD operations. Add Default rule - allow all. Send network map to peers based on rules.
This commit is contained in:
committed by
GitHub
parent
11a3863c28
commit
3ce3ccc39a
@@ -13,20 +13,33 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Groups is a handler that returns groups of the account
|
||||
type Groups struct {
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
}
|
||||
|
||||
// GroupResponse is a response sent to the client
|
||||
type GroupResponse struct {
|
||||
ID string
|
||||
Name string
|
||||
Peers []GroupPeerResponse `json:",omitempty"`
|
||||
}
|
||||
|
||||
// GroupPeerResponse is a response sent to the client
|
||||
type GroupPeerResponse struct {
|
||||
Key string
|
||||
Name string
|
||||
}
|
||||
|
||||
// GroupRequest to create or update group
|
||||
type GroupRequest struct {
|
||||
ID string
|
||||
Name string
|
||||
Peers []string
|
||||
}
|
||||
|
||||
// Groups is a handler that returns groups of the account
|
||||
type Groups struct {
|
||||
jwtExtractor jwtclaims.ClaimsExtractor
|
||||
accountManager server.AccountManager
|
||||
authAudience string
|
||||
}
|
||||
|
||||
func NewGroups(accountManager server.AccountManager, authAudience string) *Groups {
|
||||
return &Groups{
|
||||
accountManager: accountManager,
|
||||
@@ -44,7 +57,12 @@ func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, account.Groups)
|
||||
var groups []*GroupResponse
|
||||
for _, g := range account.Groups {
|
||||
groups = append(groups, toGroupResponse(account, g))
|
||||
}
|
||||
|
||||
writeJSONObject(w, groups)
|
||||
}
|
||||
|
||||
func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -54,7 +72,7 @@ func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
var req server.Group
|
||||
var req GroupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -64,13 +82,19 @@ func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Reque
|
||||
req.ID = xid.New().String()
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveGroup(account.Id, &req); err != nil {
|
||||
group := server.Group{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Peers: req.Peers,
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
|
||||
log.Errorf("failed updating group %s under account %s %v", req.ID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, &req)
|
||||
writeJSONObject(w, toGroupResponse(account, &group))
|
||||
}
|
||||
|
||||
func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -117,7 +141,7 @@ func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, group)
|
||||
writeJSONObject(w, toGroupResponse(account, group))
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
@@ -133,3 +157,29 @@ func (h *Groups) getGroupAccount(r *http.Request) (*server.Account, error) {
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func toGroupResponse(account *server.Account, group *server.Group) *GroupResponse {
|
||||
cache := make(map[string]GroupPeerResponse)
|
||||
gr := GroupResponse{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
}
|
||||
|
||||
for _, pid := range group.Peers {
|
||||
peerResp, ok := cache[pid]
|
||||
if !ok {
|
||||
peer, ok := account.Peers[pid]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
peerResp = GroupPeerResponse{
|
||||
Key: peer.Key,
|
||||
Name: peer.Name,
|
||||
}
|
||||
cache[pid] = peerResp
|
||||
}
|
||||
gr.Peers = append(gr.Peers, peerResp)
|
||||
}
|
||||
|
||||
return &gr
|
||||
}
|
||||
|
||||
211
management/server/http/handler/rules.go
Normal file
211
management/server/http/handler/rules.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const FlowBidirectString = "bidirect"
|
||||
|
||||
// RuleResponse is a response sent to the client
|
||||
type RuleResponse struct {
|
||||
ID string
|
||||
Name string
|
||||
Source []RuleGroupResponse
|
||||
Destination []RuleGroupResponse
|
||||
Flow string
|
||||
}
|
||||
|
||||
// RuleGroupResponse is a response sent to the client
|
||||
type RuleGroupResponse struct {
|
||||
ID string
|
||||
Name string
|
||||
PeersCount int
|
||||
}
|
||||
|
||||
// RuleRequest to create or update rule
|
||||
type RuleRequest struct {
|
||||
ID string
|
||||
Name string
|
||||
Source []string
|
||||
Destination []string
|
||||
Flow string
|
||||
}
|
||||
|
||||
// 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) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var rules []*RuleResponse
|
||||
for _, r := range account.Rules {
|
||||
rules = append(rules, toRuleResponse(account, r))
|
||||
}
|
||||
|
||||
writeJSONObject(w, rules)
|
||||
}
|
||||
|
||||
func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req RuleRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
req.ID = xid.New().String()
|
||||
}
|
||||
|
||||
rule := server.Rule{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Source: req.Source,
|
||||
Destination: req.Destination,
|
||||
}
|
||||
|
||||
switch req.Flow {
|
||||
case FlowBidirectString:
|
||||
rule.Flow = server.TrafficFlowBidirect
|
||||
default:
|
||||
http.Error(w, "unknown flow type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.SaveRule(account.Id, &rule); err != nil {
|
||||
log.Errorf("failed updating rule %s under account %s %v", req.ID, account.Id, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, &req)
|
||||
}
|
||||
|
||||
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
aID := account.Id
|
||||
|
||||
rID := mux.Vars(r)["id"]
|
||||
if len(rID) == 0 {
|
||||
http.Error(w, "invalid rule ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.DeleteRule(aID, rID); err != nil {
|
||||
log.Errorf("failed delete rule %s under account %s %v", rID, aID, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, "")
|
||||
}
|
||||
|
||||
func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account, err := h.getRuleAccount(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
ruleID := mux.Vars(r)["id"]
|
||||
if len(ruleID) == 0 {
|
||||
http.Error(w, "invalid rule ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := h.accountManager.GetRule(account.Id, ruleID)
|
||||
if err != nil {
|
||||
http.Error(w, "rule not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObject(w, toRuleResponse(account, rule))
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Rules) getRuleAccount(r *http.Request) (*server.Account, error) {
|
||||
jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
|
||||
|
||||
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func toRuleResponse(account *server.Account, rule *server.Rule) *RuleResponse {
|
||||
gr := RuleResponse{
|
||||
ID: rule.ID,
|
||||
Name: rule.Name,
|
||||
}
|
||||
|
||||
switch rule.Flow {
|
||||
case server.TrafficFlowBidirect:
|
||||
gr.Flow = FlowBidirectString
|
||||
default:
|
||||
gr.Flow = "unknown"
|
||||
}
|
||||
|
||||
for _, gid := range rule.Source {
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
gr.Source = append(gr.Source, RuleGroupResponse{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, gid := range rule.Destination {
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
gr.Destination = append(gr.Destination, RuleGroupResponse{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &gr
|
||||
}
|
||||
211
management/server/http/handler/rules_test.go
Normal file
211
management/server/http/handler/rules_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"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"
|
||||
)
|
||||
|
||||
func initRulesTestData(rules ...*server.Rule) *Rules {
|
||||
return &Rules{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
SaveRuleFunc: func(_ string, rule *server.Rule) error {
|
||||
if !strings.HasPrefix(rule.ID, "id-") {
|
||||
rule.ID = "id-was-set"
|
||||
}
|
||||
return nil
|
||||
},
|
||||
GetRuleFunc: func(_, ruleID string) (*server.Rule, error) {
|
||||
if ruleID != "idoftherule" {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
return &server.Rule{
|
||||
ID: "idoftherule",
|
||||
Name: "Rule",
|
||||
Source: []string{"idofsrcrule"},
|
||||
Destination: []string{"idofdestrule"},
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
}, nil
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return &server.Account{
|
||||
Id: claims.AccountId,
|
||||
Domain: "hotmail.com",
|
||||
}, 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 TestRulesGetRule(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
}{
|
||||
{
|
||||
name: "GetRule OK",
|
||||
expectedBody: true,
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/rules/idoftherule",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "GetRule not found",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/rules/notexists",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
rule := &server.Rule{
|
||||
ID: "idoftherule",
|
||||
Name: "Rule",
|
||||
}
|
||||
|
||||
p := initRulesTestData(rule)
|
||||
|
||||
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/rules/{id}", p.GetRuleHandler).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)
|
||||
}
|
||||
|
||||
var got RuleResponse
|
||||
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, rule.ID)
|
||||
assert.Equal(t, got.Name, rule.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRulesSaveRule(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
expectedRule *server.Rule
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
}{
|
||||
{
|
||||
name: "SaveRule POST OK",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/rules",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRule: &server.Rule{
|
||||
ID: "id-was-set",
|
||||
Name: "Default POSTed Rule",
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SaveRule PUT OK",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/rules",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`{"ID":"id-existed","Name":"Default POSTed Rule","Flow":"bidirect"}`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedRule: &server.Rule{
|
||||
ID: "id-existed",
|
||||
Name: "Default POSTed Rule",
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := initRulesTestData()
|
||||
|
||||
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/rules", p.CreateOrUpdateRuleHandler).Methods("PUT", "POST")
|
||||
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 := &RuleRequest{}
|
||||
if err = json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
if tc.requestType != http.MethodPost {
|
||||
assert.Equal(t, got.ID, tc.expectedRule.ID)
|
||||
}
|
||||
assert.Equal(t, got.Name, tc.expectedRule.Name)
|
||||
assert.Equal(t, got.Flow, "bidirect")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user