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
}