mirror of
https://github.com/netbirdio/netbird.git
synced 2025-05-02 07:14:48 +02:00
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
372 lines
9.7 KiB
Go
372 lines
9.7 KiB
Go
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"
|
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
|
"github.com/rs/xid"
|
|
|
|
"github.com/gorilla/mux"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// 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,
|
|
authAudience: authAudience,
|
|
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
|
}
|
|
}
|
|
|
|
// GetAllGroupsHandler list for the account
|
|
func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
|
|
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 []*api.Group
|
|
for _, g := range account.Groups {
|
|
groups = append(groups, toGroupResponse(account, g))
|
|
}
|
|
|
|
writeJSONObject(w, groups)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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 *req.Name == "" {
|
|
http.Error(w, "group name shouldn't be empty", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
|
|
group := server.Group{
|
|
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", groupID, account.Id, err)
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
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 := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
aID := account.Id
|
|
|
|
groupID := mux.Vars(r)["id"]
|
|
if len(groupID) == 0 {
|
|
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
writeJSONObject(w, "")
|
|
}
|
|
|
|
// GetGroupHandler returns a group
|
|
func (h *Groups) GetGroupHandler(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
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
groupID := mux.Vars(r)["id"]
|
|
if len(groupID) == 0 {
|
|
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
group, err := h.accountManager.GetGroup(account.Id, groupID)
|
|
if err != nil {
|
|
http.Error(w, "group not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
writeJSONObject(w, toGroupResponse(account, group))
|
|
default:
|
|
http.Error(w, "", http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func peerIPsToKeys(account *server.Account, peerIPs *[]string) []string {
|
|
var mappedPeerKeys []string
|
|
if peerIPs == nil {
|
|
return mappedPeerKeys
|
|
}
|
|
|
|
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) *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 {
|
|
_, ok := cache[pid]
|
|
if !ok {
|
|
peer, ok := account.Peers[pid]
|
|
if !ok {
|
|
continue
|
|
}
|
|
peerResp := api.PeerMinimum{
|
|
Id: peer.IP.String(),
|
|
Name: peer.Name,
|
|
}
|
|
cache[pid] = peerResp
|
|
gr.Peers = append(gr.Peers, peerResp)
|
|
}
|
|
}
|
|
return &gr
|
|
}
|