OpenAPI specification and API Adjusts (#356)

Introduced an OpenAPI specification.
Updated API handlers to use the specification types.

Added patch operation for rules and groups
and methods to the account manager.

HTTP PUT operations require id, fail if not provided.

Use snake_case for HTTP request and response body
This commit is contained in:
Maycon Santos
2022-06-14 10:32:54 +02:00
committed by GitHub
parent a454a1aa28
commit 503a116f7c
24 changed files with 2507 additions and 312 deletions
+240 -54
View File
@@ -3,6 +3,9 @@ package handler
import (
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/http/api"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http"
"github.com/netbirdio/netbird/management/server"
@@ -13,26 +16,6 @@ import (
log "github.com/sirupsen/logrus"
)
// 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
@@ -50,14 +33,14 @@ func NewGroups(accountManager server.AccountManager, authAudience string) *Group
// GetAllGroupsHandler list for the account
func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getGroupAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
var groups []*GroupResponse
var groups []*api.Group
for _, g := range account.Groups {
groups = append(groups, toGroupResponse(account, g))
}
@@ -65,31 +48,60 @@ func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, groups)
}
func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getGroupAccount(r)
// UpdateGroupHandler handles update to a group identified by a given ID
func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
var req GroupRequest
vars := mux.Vars(r)
groupID, ok := vars["id"]
if !ok {
http.Error(w, "group ID field is missing", http.StatusBadRequest)
return
}
if len(groupID) == 0 {
http.Error(w, "group ID can't be empty", http.StatusUnprocessableEntity)
return
}
_, ok = account.Groups[groupID]
if !ok {
http.Error(w, fmt.Sprintf("couldn't find group with ID %s", groupID), http.StatusNotFound)
return
}
allGroup, err := account.GetGroupAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if allGroup.ID == groupID {
http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed)
return
}
var req api.PutApiGroupsIdJSONRequestBody
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()
if *req.Name == "" {
http.Error(w, "group name shouldn't be empty", http.StatusUnprocessableEntity)
return
}
group := server.Group{
ID: req.ID,
Name: req.Name,
Peers: req.Peers,
ID: groupID,
Name: *req.Name,
Peers: peerIPsToKeys(account, 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)
log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
@@ -97,22 +109,183 @@ func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Reque
writeJSONObject(w, toGroupResponse(account, &group))
}
// PatchGroupHandler handles patch updates to a group identified by a given ID
func (h *Groups) PatchGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
vars := mux.Vars(r)
groupID := vars["id"]
if len(groupID) == 0 {
http.Error(w, "invalid group Id", http.StatusBadRequest)
return
}
_, ok := account.Groups[groupID]
if !ok {
http.Error(w, fmt.Sprintf("couldn't find group id %s", groupID), http.StatusNotFound)
return
}
allGroup, err := account.GetGroupAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if allGroup.ID == groupID {
http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed)
return
}
var req api.PatchApiGroupsIdJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(req) == 0 {
http.Error(w, "no patch instruction received", http.StatusBadRequest)
return
}
var operations []server.GroupUpdateOperation
for _, patch := range req {
switch patch.Path {
case api.GroupPatchOperationPathName:
if patch.Op != api.GroupPatchOperationOpReplace {
http.Error(w, fmt.Sprintf("Name field only accepts replace operation, got %s", patch.Op),
http.StatusBadRequest)
return
}
if len(patch.Value) == 0 || patch.Value[0] == "" {
http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity)
return
}
operations = append(operations, server.GroupUpdateOperation{
Type: server.UpdateGroupName,
Values: patch.Value,
})
case api.GroupPatchOperationPathPeers:
switch patch.Op {
case api.GroupPatchOperationOpReplace:
peerKeys := peerIPsToKeys(account, &patch.Value)
operations = append(operations, server.GroupUpdateOperation{
Type: server.UpdateGroupPeers,
Values: peerKeys,
})
case api.GroupPatchOperationOpRemove:
peerKeys := peerIPsToKeys(account, &patch.Value)
operations = append(operations, server.GroupUpdateOperation{
Type: server.RemovePeersFromGroup,
Values: peerKeys,
})
case api.GroupPatchOperationOpAdd:
peerKeys := peerIPsToKeys(account, &patch.Value)
operations = append(operations, server.GroupUpdateOperation{
Type: server.InsertPeersToGroup,
Values: peerKeys,
})
default:
http.Error(w, "invalid operation, \"%s\", for Peers field", http.StatusBadRequest)
return
}
default:
http.Error(w, "invalid patch path", http.StatusBadRequest)
return
}
}
group, err := h.accountManager.UpdateGroup(account.Id, groupID, operations)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.Internal {
http.Error(w, errStatus.String(), http.StatusInternalServerError)
return
}
if ok && errStatus.Code() == codes.NotFound {
http.Error(w, errStatus.String(), http.StatusNotFound)
return
}
log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
writeJSONObject(w, toGroupResponse(account, group))
}
// CreateGroupHandler handles group creation request
func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
var req api.PostApiGroupsJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Name == "" {
http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity)
return
}
group := server.Group{
ID: xid.New().String(),
Name: req.Name,
Peers: peerIPsToKeys(account, req.Peers),
}
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
log.Errorf("failed creating group \"%s\" under account %s %v", req.Name, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
writeJSONObject(w, toGroupResponse(account, &group))
}
// DeleteGroupHandler handles group deletion request
func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getGroupAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
aID := account.Id
gID := mux.Vars(r)["id"]
if len(gID) == 0 {
groupID := mux.Vars(r)["id"]
if len(groupID) == 0 {
http.Error(w, "invalid group ID", http.StatusBadRequest)
return
}
if err := h.accountManager.DeleteGroup(aID, gID); err != nil {
log.Errorf("failed delete group %s under account %s %v", gID, aID, err)
allGroup, err := account.GetGroupAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if allGroup.ID == groupID {
http.Error(w, "deleting group ALL is not allowed", http.StatusMethodNotAllowed)
return
}
if err := h.accountManager.DeleteGroup(aID, groupID); err != nil {
log.Errorf("failed delete group %s under account %s %v", groupID, aID, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
@@ -120,8 +293,9 @@ func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, "")
}
// GetGroupHandler returns a group
func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getGroupAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -147,39 +321,51 @@ func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
}
}
func (h *Groups) getGroupAccount(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)
func peerIPsToKeys(account *server.Account, peerIPs *[]string) []string {
var mappedPeerKeys []string
if peerIPs == nil {
return mappedPeerKeys
}
return account, nil
peersChecked := make(map[string]struct{})
for _, requestPeersIP := range *peerIPs {
_, ok := peersChecked[requestPeersIP]
if ok {
continue
}
peersChecked[requestPeersIP] = struct{}{}
for _, accountPeer := range account.Peers {
if accountPeer.IP.String() == requestPeersIP {
mappedPeerKeys = append(mappedPeerKeys, accountPeer.Key)
}
}
}
return mappedPeerKeys
}
func toGroupResponse(account *server.Account, group *server.Group) *GroupResponse {
cache := make(map[string]GroupPeerResponse)
gr := GroupResponse{
ID: group.ID,
Name: group.Name,
func toGroupResponse(account *server.Account, group *server.Group) *api.Group {
cache := make(map[string]api.PeerMinimum)
gr := api.Group{
Id: group.ID,
Name: group.Name,
PeersCount: len(group.Peers),
}
for _, pid := range group.Peers {
peerResp, ok := cache[pid]
_, ok := cache[pid]
if !ok {
peer, ok := account.Peers[pid]
if !ok {
continue
}
peerResp = GroupPeerResponse{
Key: peer.Key,
peerResp := api.PeerMinimum{
Id: peer.IP.String(),
Name: peer.Name,
}
cache[pid] = peerResp
gr.Peers = append(gr.Peers, peerResp)
}
gr.Peers = append(gr.Peers, peerResp)
}
return &gr
}
+122 -13
View File
@@ -4,7 +4,9 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/http/api"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
@@ -18,6 +20,11 @@ import (
"github.com/netbirdio/netbird/management/server/mock_server"
)
var TestPeers = map[string]*server.Peer{
"A": &server.Peer{Key: "A", IP: net.ParseIP("100.100.100.100")},
"B": &server.Peer{Key: "B", IP: net.ParseIP("200.200.200.200")},
}
func initGroupTestData(groups ...*server.Group) *Groups {
return &Groups{
accountManager: &mock_server.MockAccountManager{
@@ -36,10 +43,38 @@ func initGroupTestData(groups ...*server.Group) *Groups {
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")
},
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{
Id: claims.AccountId,
Domain: "hotmail.com",
Peers: TestPeers,
Groups: map[string]*server.Group{
"id-existed": &server.Group{ID: "id-existed", Peers: []string{"A", "B"}},
"id-all": &server.Group{ID: "id-all", Name: "All"}},
}, nil
},
},
@@ -125,41 +160,114 @@ func TestGetGroup(t *testing.T) {
}
}
func TestSaveGroup(t *testing.T) {
func TestWriteGroup(t *testing.T) {
tt := []struct {
name string
expectedStatus int
expectedBody bool
expectedGroup *server.Group
expectedGroup *api.Group
requestType string
requestPath string
requestBody io.Reader
}{
{
name: "SaveGroup POST OK",
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: &server.Group{
ID: "id-was-set",
expectedGroup: &api.Group{
Id: "id-was-set",
Name: "Default POSTed Group",
},
},
{
name: "SaveGroup PUT OK",
requestType: http.MethodPut,
name: "Write Group POST Invalid Name",
requestType: http.MethodPost,
requestPath: "/api/groups",
requestBody: bytes.NewBuffer(
[]byte(`{"ID":"id-existed","Name":"Default POSTed Group"}`)),
[]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: &server.Group{
ID: "id-existed",
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.StatusMethodNotAllowed,
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.StatusBadRequest,
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"}},
},
},
}
p := initGroupTestData()
@@ -170,7 +278,9 @@ func TestSaveGroup(t *testing.T) {
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
router := mux.NewRouter()
router.HandleFunc("/api/groups", p.CreateOrUpdateGroupHandler).Methods("PUT", "POST")
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()
@@ -191,11 +301,10 @@ func TestSaveGroup(t *testing.T) {
return
}
got := &server.Group{}
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)
})
}
+40 -45
View File
@@ -3,13 +3,12 @@ package handler
import (
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/jwtclaims"
log "github.com/sirupsen/logrus"
"net/http"
)
//Peers is a handler that returns peers of the account
@@ -19,21 +18,6 @@ type Peers struct {
jwtExtractor jwtclaims.ClaimsExtractor
}
//PeerResponse is a response sent to the client
type PeerResponse struct {
Name string
IP string
Connected bool
LastSeen time.Time
OS string
Version string
}
//PeerRequest is a request sent by the client
type PeerRequest struct {
Name string
}
func NewPeers(accountManager server.AccountManager, authAudience string) *Peers {
return &Peers{
accountManager: accountManager,
@@ -42,21 +26,21 @@ func NewPeers(accountManager server.AccountManager, authAudience string) *Peers
}
}
func (h *Peers) updatePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
req := &PeerRequest{}
func (h *Peers) updatePeer(account *server.Account, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
req := &api.PutApiPeersIdJSONBody{}
peerIp := peer.IP
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
peer, err = h.accountManager.RenamePeer(accountId, peer.Key, req.Name)
peer, err = h.accountManager.RenamePeer(account.Id, peer.Key, req.Name)
if err != nil {
log.Errorf("failed updating peer %s under account %s %v", peerIp, accountId, err)
log.Errorf("failed updating peer %s under account %s %v", peerIp, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
writeJSONObject(w, toPeerResponse(peer))
writeJSONObject(w, toPeerResponse(peer, account))
}
func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
@@ -69,19 +53,8 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW
writeJSONObject(w, "")
}
func (h *Peers) getPeerAccount(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 (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
account, err := h.getPeerAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -105,10 +78,10 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
h.deletePeer(account.Id, peer, w, r)
return
case http.MethodPut:
h.updatePeer(account.Id, peer, w, r)
h.updatePeer(account, peer, w, r)
return
case http.MethodGet:
writeJSONObject(w, toPeerResponse(peer))
writeJSONObject(w, toPeerResponse(peer, account))
return
default:
@@ -120,16 +93,16 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
account, err := h.getPeerAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
respBody := []*PeerResponse{}
respBody := []*api.Peer{}
for _, peer := range account.Peers {
respBody = append(respBody, toPeerResponse(peer))
respBody = append(respBody, toPeerResponse(peer, account))
}
writeJSONObject(w, respBody)
return
@@ -138,13 +111,35 @@ func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
}
}
func toPeerResponse(peer *server.Peer) *PeerResponse {
return &PeerResponse{
func toPeerResponse(peer *server.Peer, account *server.Account) *api.Peer {
var groupsInfo []api.GroupMinimum
groupsChecked := make(map[string]struct{})
for _, group := range account.Groups {
_, ok := groupsChecked[group.ID]
if ok {
continue
}
groupsChecked[group.ID] = struct{}{}
for _, pk := range group.Peers {
if pk == peer.Key {
info := api.GroupMinimum{
Id: group.ID,
Name: group.Name,
PeersCount: len(group.Peers),
}
groupsInfo = append(groupsInfo, info)
break
}
}
}
return &api.Peer{
Id: peer.IP.String(),
Name: peer.Name,
IP: peer.IP.String(),
Ip: peer.IP.String(),
Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen,
OS: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core),
Os: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core),
Version: peer.Meta.WtVersion,
Groups: groupsInfo,
}
}
+4 -3
View File
@@ -2,6 +2,7 @@ package handler
import (
"encoding/json"
"github.com/netbirdio/netbird/management/server/http/api"
"io"
"net"
"net/http"
@@ -98,7 +99,7 @@ func TestGetPeers(t *testing.T) {
t.Fatalf("I don't know what I expected; %v", err)
}
respBody := []*PeerResponse{}
respBody := []*api.Peer{}
err = json.Unmarshal(content, &respBody)
if err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
@@ -107,8 +108,8 @@ func TestGetPeers(t *testing.T) {
got := respBody[0]
assert.Equal(t, got.Name, peer.Name)
assert.Equal(t, got.Version, peer.Meta.WtVersion)
assert.Equal(t, got.IP, peer.IP.String())
assert.Equal(t, got.OS, "OS core")
assert.Equal(t, got.Ip, peer.IP.String())
assert.Equal(t, got.Os, "OS core")
})
}
}
+294 -68
View File
@@ -3,43 +3,17 @@ package handler
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/rs/xid"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http"
)
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
@@ -57,14 +31,14 @@ func NewRules(accountManager server.AccountManager, authAudience string) *Rules
// GetAllRulesHandler list for the account
func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getRuleAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
rules := []*RuleResponse{}
rules := []*api.Rule{}
for _, r := range account.Rules {
rules = append(rules, toRuleResponse(account, r))
}
@@ -72,32 +46,59 @@ func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, rules)
}
func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getRuleAccount(r)
// UpdateRuleHandler handles update to a rule identified by a given ID
func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
var req RuleRequest
vars := mux.Vars(r)
ruleID := vars["id"]
if len(ruleID) == 0 {
http.Error(w, "invalid rule Id", http.StatusBadRequest)
return
}
_, ok := account.Rules[ruleID]
if !ok {
http.Error(w, fmt.Sprintf("couldn't find rule id %s", ruleID), http.StatusNotFound)
return
}
var req api.PutApiRulesIdJSONRequestBody
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()
if req.Name == "" {
http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity)
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: req.ID,
ID: ruleID,
Name: req.Name,
Source: req.Source,
Destination: req.Destination,
Source: reqSources,
Destination: reqDestinations,
Disabled: req.Disabled,
Description: req.Description,
}
switch req.Flow {
case FlowBidirectString:
case server.TrafficFlowBidirectString:
rule.Flow = server.TrafficFlowBidirect
default:
http.Error(w, "unknown flow type", http.StatusBadRequest)
@@ -105,16 +106,233 @@ func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request
}
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)
log.Errorf("failed updating rule \"%s\" under account %s %v", ruleID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
writeJSONObject(w, &req)
resp := toRuleResponse(account, &rule)
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) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
vars := mux.Vars(r)
ruleID := vars["id"]
if len(ruleID) == 0 {
http.Error(w, "invalid rule Id", http.StatusBadRequest)
return
}
_, ok := account.Rules[ruleID]
if !ok {
http.Error(w, fmt.Sprintf("couldn't find rule id %s", ruleID), http.StatusNotFound)
return
}
var req api.PatchApiRulesIdJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(req) == 0 {
http.Error(w, "no patch instruction received", http.StatusBadRequest)
return
}
var operations []server.RuleUpdateOperation
for _, patch := range req {
switch patch.Path {
case api.RulePatchOperationPathName:
if patch.Op != api.RulePatchOperationOpReplace {
http.Error(w, fmt.Sprintf("Name field only accepts replace operation, got %s", patch.Op),
http.StatusBadRequest)
return
}
if len(patch.Value) == 0 || patch.Value[0] == "" {
http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity)
return
}
operations = append(operations, server.RuleUpdateOperation{
Type: server.UpdateRuleName,
Values: patch.Value,
})
case api.RulePatchOperationPathDescription:
if patch.Op != api.RulePatchOperationOpReplace {
http.Error(w, fmt.Sprintf("Description field only accepts replace operation, got %s", patch.Op),
http.StatusBadRequest)
return
}
operations = append(operations, server.RuleUpdateOperation{
Type: server.UpdateRuleDescription,
Values: patch.Value,
})
case api.RulePatchOperationPathFlow:
if patch.Op != api.RulePatchOperationOpReplace {
http.Error(w, fmt.Sprintf("Flow field only accepts replace operation, got %s", patch.Op),
http.StatusBadRequest)
return
}
operations = append(operations, server.RuleUpdateOperation{
Type: server.UpdateRuleFlow,
Values: patch.Value,
})
case api.RulePatchOperationPathDisabled:
if patch.Op != api.RulePatchOperationOpReplace {
http.Error(w, fmt.Sprintf("Disabled field only accepts replace operation, got %s", patch.Op),
http.StatusBadRequest)
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:
http.Error(w, "invalid operation, \"%s\", for Source field", http.StatusBadRequest)
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:
http.Error(w, "invalid operation, \"%s\", for Destination field", http.StatusBadRequest)
return
}
default:
http.Error(w, "invalid patch path", http.StatusBadRequest)
return
}
}
rule, err := h.accountManager.UpdateRule(account.Id, ruleID, operations)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.Internal {
http.Error(w, errStatus.String(), http.StatusInternalServerError)
return
}
if ok && errStatus.Code() == codes.NotFound {
http.Error(w, errStatus.String(), http.StatusNotFound)
return
}
if ok && errStatus.Code() == codes.InvalidArgument {
http.Error(w, errStatus.String(), http.StatusBadRequest)
return
}
log.Errorf("failed updating rule %s under account %s %v", ruleID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
resp := toRuleResponse(account, rule)
writeJSONObject(w, &resp)
}
// CreateRuleHandler handles rule creation request
func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
var req api.PostApiRulesJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Name == "" {
http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity)
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:
http.Error(w, "unknown flow type", http.StatusBadRequest)
return
}
if err := h.accountManager.SaveRule(account.Id, &rule); err != nil {
log.Errorf("failed creating rule \"%s\" under account %s %v", req.Name, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
resp := toRuleResponse(account, &rule)
writeJSONObject(w, &resp)
}
// DeleteRuleHandler handles rule deletion request
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getRuleAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -136,8 +354,9 @@ func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, "")
}
// GetRuleHandler handles a group Get request identified by ID
func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := h.getRuleAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -163,47 +382,54 @@ func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
}
}
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,
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 = FlowBidirectString
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 {
gr.Source = append(gr.Source, RuleGroupResponse{
ID: group.ID,
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 {
gr.Destination = append(gr.Destination, RuleGroupResponse{
ID: group.ID,
minimum := api.GroupMinimum{
Id: group.ID,
Name: group.Name,
PeersCount: len(group.Peers),
})
}
gr.Destinations = append(gr.Destinations, minimum)
cache[gid] = minimum
}
}
+118 -21
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/http/api"
"io"
"net/http"
"net/http/httptest"
@@ -39,10 +40,41 @@ func initRulesTestData(rules ...*server.Rule) *Rules {
Flow: server.TrafficFlowBidirect,
}, nil
},
UpdateRuleFunc: func(_ string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error) {
var rule server.Rule
rule.ID = ruleID
for _, operation := range operations {
switch operation.Type {
case server.UpdateRuleName:
rule.Name = operation.Values[0]
case server.UpdateRuleDescription:
rule.Description = operation.Values[0]
case server.UpdateRuleFlow:
if server.TrafficFlowBidirectString == operation.Values[0] {
rule.Flow = server.TrafficFlowBidirect
} else {
rule.Flow = 100
}
case server.UpdateSourceGroups, server.InsertGroupsToSource:
rule.Source = operation.Values
case server.UpdateDestinationGroups, server.InsertGroupsToDestination:
rule.Destination = operation.Values
case server.RemoveGroupsFromSource, server.RemoveGroupsFromDestination:
default:
return nil, fmt.Errorf("no operation")
}
}
return &rule, nil
},
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{
Id: claims.AccountId,
Domain: "hotmail.com",
Rules: map[string]*server.Rule{"id-existed": &server.Rule{ID: "id-existed"}},
Groups: map[string]*server.Group{
"F": &server.Group{ID: "F"},
"G": &server.Group{ID: "G"},
},
}, nil
},
},
@@ -117,52 +149,118 @@ func TestRulesGetRule(t *testing.T) {
t.Fatalf("I don't know what I expected; %v", err)
}
var got RuleResponse
var got api.Rule
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.Id, rule.ID)
assert.Equal(t, got.Name, rule.Name)
})
}
}
func TestRulesSaveRule(t *testing.T) {
func TestRulesWriteRule(t *testing.T) {
tt := []struct {
name string
expectedStatus int
expectedBody bool
expectedRule *server.Rule
expectedRule *api.Rule
requestType string
requestPath string
requestBody io.Reader
}{
{
name: "SaveRule POST OK",
name: "WriteRule 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",
expectedRule: &api.Rule{
Id: "id-was-set",
Name: "Default POSTed Rule",
Flow: server.TrafficFlowBidirect,
Flow: server.TrafficFlowBidirectString,
},
},
{
name: "SaveRule PUT OK",
requestType: http.MethodPut,
name: "WriteRule POST Invalid Name",
requestType: http.MethodPost,
requestPath: "/api/rules",
requestBody: bytes.NewBuffer(
[]byte(`{"ID":"id-existed","Name":"Default POSTed Rule","Flow":"bidirect"}`)),
[]byte(`{"Name":"","Flow":"bidirect"}`)),
expectedStatus: http.StatusUnprocessableEntity,
expectedBody: false,
},
{
name: "WriteRule PUT OK",
requestType: http.MethodPut,
requestPath: "/api/rules/id-existed",
requestBody: bytes.NewBuffer(
[]byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)),
expectedStatus: http.StatusOK,
expectedRule: &server.Rule{
ID: "id-existed",
expectedBody: true,
expectedRule: &api.Rule{
Id: "id-existed",
Name: "Default POSTed Rule",
Flow: server.TrafficFlowBidirect,
Flow: server.TrafficFlowBidirectString,
},
},
{
name: "WriteRule PUT Invalid Name",
requestType: http.MethodPut,
requestPath: "/api/rules/id-existed",
requestBody: bytes.NewBuffer(
[]byte(`{"Name":"","Flow":"bidirect"}`)),
expectedStatus: http.StatusUnprocessableEntity,
},
{
name: "Write Rule PATCH Name OK",
requestType: http.MethodPatch,
requestPath: "/api/rules/id-existed",
requestBody: bytes.NewBuffer(
[]byte(`[{"op":"replace","path":"name","value":["Default POSTed Rule"]}]`)),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedRule: &api.Rule{
Id: "id-existed",
Name: "Default POSTed Rule",
Flow: server.TrafficFlowBidirectString,
},
},
{
name: "Write Rule PATCH Invalid Name OP",
requestType: http.MethodPatch,
requestPath: "/api/rules/id-existed",
requestBody: bytes.NewBuffer(
[]byte(`[{"op":"insert","path":"name","value":[""]}]`)),
expectedStatus: http.StatusBadRequest,
expectedBody: false,
},
{
name: "Write Rule PATCH Invalid Name",
requestType: http.MethodPatch,
requestPath: "/api/rules/id-existed",
requestBody: bytes.NewBuffer(
[]byte(`[{"op":"replace","path":"name","value":[]}]`)),
expectedStatus: http.StatusUnprocessableEntity,
expectedBody: false,
},
{
name: "Write Rule PATCH Sources OK",
requestType: http.MethodPatch,
requestPath: "/api/rules/id-existed",
requestBody: bytes.NewBuffer(
[]byte(`[{"op":"replace","path":"sources","value":["G","F"]}]`)),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedRule: &api.Rule{
Id: "id-existed",
Flow: server.TrafficFlowBidirectString,
Sources: []api.GroupMinimum{
{Id: "G"},
{Id: "F"}},
},
},
}
@@ -175,7 +273,9 @@ func TestRulesSaveRule(t *testing.T) {
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
router := mux.NewRouter()
router.HandleFunc("/api/rules", p.CreateOrUpdateRuleHandler).Methods("PUT", "POST")
router.HandleFunc("/api/rules", p.CreateRuleHandler).Methods("POST")
router.HandleFunc("/api/rules/{id}", p.UpdateRuleHandler).Methods("PUT")
router.HandleFunc("/api/rules/{id}", p.PatchRuleHandler).Methods("PATCH")
router.ServeHTTP(recorder, req)
res := recorder.Result()
@@ -196,16 +296,13 @@ func TestRulesSaveRule(t *testing.T) {
return
}
got := &RuleRequest{}
got := &api.Rule{}
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")
assert.Equal(t, got, tc.expectedRule)
})
}
}
+25 -50
View File
@@ -2,56 +2,34 @@ package handler
import (
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/jwtclaims"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http"
"time"
)
// SetupKeys is a handler that returns a list of setup keys of the account
type SetupKeys struct {
accountManager server.AccountManager
jwtExtractor jwtclaims.ClaimsExtractor
authAudience string
}
// SetupKeyResponse is a response sent to the client
type SetupKeyResponse struct {
Id string
Key string
Name string
Expires time.Time
Type server.SetupKeyType
Valid bool
Revoked bool
UsedTimes int
LastUsed time.Time
State string
}
// SetupKeyRequest is a request sent by client. This object contains fields that can be modified
type SetupKeyRequest struct {
Name string
Type server.SetupKeyType
ExpiresIn *util.Duration
Revoked bool
}
func NewSetupKeysHandler(accountManager server.AccountManager, authAudience string) *SetupKeys {
return &SetupKeys{
accountManager: accountManager,
authAudience: authAudience,
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
}
}
func (h *SetupKeys) updateKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) {
req := &SetupKeyRequest{}
req := &api.PutApiSetupKeysIdJSONRequestBody{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -96,19 +74,28 @@ func (h *SetupKeys) getKey(accountId string, keyId string, w http.ResponseWriter
}
func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.Request) {
req := &SetupKeyRequest{}
req := &api.PostApiSetupKeysJSONRequestBody{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !(req.Type == server.SetupKeyReusable || req.Type == server.SetupKeyOneOff) {
if req.Name == "" {
http.Error(w, "Setup key name shouldn't be empty", http.StatusUnprocessableEntity)
return
}
if !(server.SetupKeyType(req.Type) == server.SetupKeyReusable ||
server.SetupKeyType(req.Type) == server.SetupKeyOneOff) {
http.Error(w, "unknown setup key type "+string(req.Type), http.StatusBadRequest)
return
}
setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, req.Type, req.ExpiresIn)
expiresIn := time.Duration(req.ExpiresIn) * time.Second
setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, server.SetupKeyType(req.Type), expiresIn)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.NotFound {
@@ -122,20 +109,8 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
writeSuccess(w, setupKey)
}
func (h *SetupKeys) getSetupKeyAccount(r *http.Request) (*server.Account, error) {
extractor := jwtclaims.NewClaimsExtractor(nil)
jwtClaims := extractor.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 (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
account, err := h.getSetupKeyAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -163,7 +138,7 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
account, err := h.getSetupKeyAccount(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -178,7 +153,7 @@ func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
respBody := []*SetupKeyResponse{}
respBody := []*api.SetupKey{}
for _, key := range account.SetupKeys {
respBody = append(respBody, toResponseBody(key))
}
@@ -204,7 +179,7 @@ func writeSuccess(w http.ResponseWriter, key *server.SetupKey) {
}
}
func toResponseBody(key *server.SetupKey) *SetupKeyResponse {
func toResponseBody(key *server.SetupKey) *api.SetupKey {
var state string
if key.IsExpired() {
state = "expired"
@@ -215,12 +190,12 @@ func toResponseBody(key *server.SetupKey) *SetupKeyResponse {
} else {
state = "valid"
}
return &SetupKeyResponse{
return &api.SetupKey{
Id: key.Id,
Key: key.Key,
Name: key.Name,
Expires: key.ExpiresAt,
Type: key.Type,
Type: string(key.Type),
Valid: key.IsValid(),
Revoked: key.Revoked,
UsedTimes: key.UsedTimes,
+8 -26
View File
@@ -1,7 +1,7 @@
package handler
import (
"fmt"
"github.com/netbirdio/netbird/management/server/http/api"
"net/http"
log "github.com/sirupsen/logrus"
@@ -16,13 +16,6 @@ type UserHandler struct {
jwtExtractor jwtclaims.ClaimsExtractor
}
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Role string `json:"role"`
}
func NewUserHandler(accountManager server.AccountManager, authAudience string) *UserHandler {
return &UserHandler{
accountManager: accountManager,
@@ -31,37 +24,26 @@ func NewUserHandler(accountManager server.AccountManager, authAudience string) *
}
}
func (u *UserHandler) getAccountId(r *http.Request) (*server.Account, error) {
jwtClaims := u.jwtExtractor.ExtractClaimsFromRequestContext(r, u.authAudience)
account, err := u.accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
if err != nil {
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
}
return account, nil
}
// GetUsers returns a list of users of the account this user belongs to.
// It also gathers additional user data (like email and name) from the IDP manager.
func (u *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "", http.StatusBadRequest)
}
account, err := u.getAccountId(r)
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
}
data, err := u.accountManager.GetUsersFromAccount(account.Id)
data, err := h.accountManager.GetUsersFromAccount(account.Id)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
users := []*UserResponse{}
users := []*api.User{}
for _, r := range data {
users = append(users, toUserResponse(r))
}
@@ -69,9 +51,9 @@ func (u *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, users)
}
func toUserResponse(user *server.UserInfo) *UserResponse {
return &UserResponse{
ID: user.ID,
func toUserResponse(user *server.UserInfo) *api.User {
return &api.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
Role: user.Role,
+17
View File
@@ -3,6 +3,9 @@ package handler
import (
"encoding/json"
"errors"
"fmt"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"net/http"
"time"
)
@@ -47,3 +50,17 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
return errors.New("invalid duration")
}
}
func getJWTAccount(accountManager server.AccountManager,
jwtExtractor jwtclaims.ClaimsExtractor,
authAudience string, r *http.Request) (*server.Account, error) {
jwtClaims := jwtExtractor.ExtractClaimsFromRequestContext(r, authAudience)
account, err := accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
if err != nil {
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
}
return account, nil
}