mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
[management] Groups API with name query parameter (#4831)
This commit is contained in:
@@ -48,6 +48,29 @@ func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||||
|
|
||||||
|
// Check if filtering by name
|
||||||
|
groupName := r.URL.Query().Get("name")
|
||||||
|
if groupName != "" {
|
||||||
|
// Get single group by name
|
||||||
|
group, err := h.accountManager.GetGroupByName(r.Context(), groupName, accountID)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accountPeers, err := h.accountManager.GetPeers(r.Context(), accountID, userID, "", "")
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as array with single element to maintain API consistency
|
||||||
|
groupsResponse := []*api.Group{toGroupResponse(accountPeers, group)}
|
||||||
|
util.WriteJSONObject(r.Context(), w, groupsResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all groups
|
||||||
groups, err := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
|
groups, err := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(r.Context(), err, w)
|
util.WriteError(r.Context(), err, w)
|
||||||
|
|||||||
@@ -60,12 +60,23 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
|
|||||||
|
|
||||||
return group, nil
|
return group, nil
|
||||||
},
|
},
|
||||||
|
GetAllGroupsFunc: func(ctx context.Context, accountID, userID string) ([]*types.Group, error) {
|
||||||
|
groups := []*types.Group{
|
||||||
|
{ID: "id-jwt-group", Name: "From JWT", Issued: types.GroupIssuedJWT},
|
||||||
|
{ID: "id-existed", Name: "Existed", Peers: []string{"A", "B"}, Issued: types.GroupIssuedAPI},
|
||||||
|
{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI},
|
||||||
|
}
|
||||||
|
|
||||||
|
groups = append(groups, initGroups...)
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
},
|
||||||
GetGroupByNameFunc: func(ctx context.Context, groupName, _ string) (*types.Group, error) {
|
GetGroupByNameFunc: func(ctx context.Context, groupName, _ string) (*types.Group, error) {
|
||||||
if groupName == "All" {
|
if groupName == "All" {
|
||||||
return &types.Group{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI}, nil
|
return &types.Group{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unknown group name")
|
return nil, status.Errorf(status.NotFound, "unknown group name")
|
||||||
},
|
},
|
||||||
GetPeersFunc: func(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
|
GetPeersFunc: func(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
|
||||||
return maps.Values(TestPeers), nil
|
return maps.Values(TestPeers), nil
|
||||||
@@ -287,6 +298,84 @@ func TestWriteGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAllGroups(t *testing.T) {
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
expectedStatus int
|
||||||
|
expectedBody bool
|
||||||
|
requestType string
|
||||||
|
requestPath string
|
||||||
|
expectedCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Get All Groups",
|
||||||
|
expectedBody: true,
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/groups",
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedCount: 3, // id-jwt-group, id-existed, id-all
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get Group By Name - Existing",
|
||||||
|
expectedBody: true,
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/groups?name=All",
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get Group By Name - Not Found",
|
||||||
|
expectedBody: false,
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/groups?name=NonExistent",
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p := initGroupTestData()
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
|
||||||
|
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||||
|
UserId: "test_user",
|
||||||
|
Domain: "hotmail.com",
|
||||||
|
AccountId: "test_id",
|
||||||
|
})
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/api/groups", p.getAllGroups).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("Failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []api.Group
|
||||||
|
if err = json.Unmarshal(content, &groups); err != nil {
|
||||||
|
t.Fatalf("Response is not in correct json format; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedCount, len(groups))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeleteGroup(t *testing.T) {
|
func TestDeleteGroup(t *testing.T) {
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrGroupNotFound is returned when a group is not found
|
||||||
|
var ErrGroupNotFound = errors.New("group not found")
|
||||||
|
|
||||||
// GroupsAPI APIs for Groups, do not use directly
|
// GroupsAPI APIs for Groups, do not use directly
|
||||||
type GroupsAPI struct {
|
type GroupsAPI struct {
|
||||||
c *Client
|
c *Client
|
||||||
@@ -27,6 +31,27 @@ func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) {
|
|||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByName get group by name
|
||||||
|
// See more: https://docs.netbird.io/api/resources/groups#list-all-groups
|
||||||
|
func (a *GroupsAPI) GetByName(ctx context.Context, groupName string) (*api.Group, error) {
|
||||||
|
params := map[string]string{"name": groupName}
|
||||||
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/groups", nil, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.Body != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
ret, err := parseResponse[[]api.Group](resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, ErrGroupNotFound
|
||||||
|
}
|
||||||
|
return &ret[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get get group info
|
// Get get group info
|
||||||
// See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group
|
// See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group
|
||||||
func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) {
|
func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) {
|
||||||
|
|||||||
@@ -3362,6 +3362,14 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- BearerAuth: [ ]
|
- BearerAuth: [ ]
|
||||||
- TokenAuth: [ ]
|
- TokenAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: name
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Filter groups by name (exact match)
|
||||||
|
example: "devs"
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: A JSON Array of Groups
|
description: A JSON Array of Groups
|
||||||
@@ -3375,6 +3383,8 @@ paths:
|
|||||||
"$ref": "#/components/responses/bad_request"
|
"$ref": "#/components/responses/bad_request"
|
||||||
'401':
|
'401':
|
||||||
"$ref": "#/components/responses/requires_authentication"
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'404':
|
||||||
|
"$ref": "#/components/responses/not_found"
|
||||||
'403':
|
'403':
|
||||||
"$ref": "#/components/responses/forbidden"
|
"$ref": "#/components/responses/forbidden"
|
||||||
'500':
|
'500':
|
||||||
|
|||||||
@@ -1908,6 +1908,12 @@ type GetApiEventsNetworkTrafficParamsConnectionType string
|
|||||||
// GetApiEventsNetworkTrafficParamsDirection defines parameters for GetApiEventsNetworkTraffic.
|
// GetApiEventsNetworkTrafficParamsDirection defines parameters for GetApiEventsNetworkTraffic.
|
||||||
type GetApiEventsNetworkTrafficParamsDirection string
|
type GetApiEventsNetworkTrafficParamsDirection string
|
||||||
|
|
||||||
|
// GetApiGroupsParams defines parameters for GetApiGroups.
|
||||||
|
type GetApiGroupsParams struct {
|
||||||
|
// Name Filter groups by name (exact match)
|
||||||
|
Name *string `form:"name,omitempty" json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetApiPeersParams defines parameters for GetApiPeers.
|
// GetApiPeersParams defines parameters for GetApiPeers.
|
||||||
type GetApiPeersParams struct {
|
type GetApiPeersParams struct {
|
||||||
// Name Filter peers by name
|
// Name Filter peers by name
|
||||||
|
|||||||
Reference in New Issue
Block a user