change api

This commit is contained in:
crn4
2026-03-19 21:49:04 +01:00
parent da57b0f276
commit 177171e437
5 changed files with 37 additions and 493 deletions

View File

@@ -1,150 +0,0 @@
package selfhostedproxy
import (
"net/http"
"github.com/gorilla/mux"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/permissions/modules"
"github.com/netbirdio/netbird/management/server/permissions/operations"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/http/util"
"github.com/netbirdio/netbird/shared/management/status"
)
// ProxyDisconnector can force-disconnect a connected proxy's gRPC stream.
type ProxyDisconnector interface {
ForceDisconnect(proxyID string)
}
type handler struct {
proxyMgr proxy.Manager
serviceMgr rpservice.Manager
permissionsManager permissions.Manager
disconnector ProxyDisconnector
}
func RegisterEndpoints(proxyMgr proxy.Manager, serviceMgr rpservice.Manager, permissionsManager permissions.Manager, disconnector ProxyDisconnector, router *mux.Router) {
h := &handler{
proxyMgr: proxyMgr,
serviceMgr: serviceMgr,
permissionsManager: permissionsManager,
disconnector: disconnector,
}
router.HandleFunc("/reverse-proxies/self-hosted-proxies", h.listProxies).Methods("GET", "OPTIONS")
router.HandleFunc("/reverse-proxies/self-hosted-proxies/{proxyId}", h.deleteProxy).Methods("DELETE", "OPTIONS")
}
func (h *handler) listProxies(w http.ResponseWriter, r *http.Request) {
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
ok, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Read)
if err != nil {
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
return
}
if !ok {
util.WriteErrorResponse("permission denied", http.StatusForbidden, w)
return
}
p, err := h.proxyMgr.GetAccountProxy(r.Context(), userAuth.AccountId)
if err != nil {
if isNotFound(err) {
util.WriteJSONObject(r.Context(), w, []api.SelfHostedProxy{})
return
}
util.WriteErrorResponse("failed to get proxy", http.StatusInternalServerError, w)
return
}
serviceCount := 0
services, err := h.serviceMgr.GetAccountServices(r.Context(), userAuth.AccountId)
if err == nil {
for _, svc := range services {
if svc.ProxyCluster == p.ClusterAddress {
serviceCount++
}
}
}
resp := []api.SelfHostedProxy{toSelfHostedProxyResponse(p, serviceCount)}
util.WriteJSONObject(r.Context(), w, resp)
}
func (h *handler) deleteProxy(w http.ResponseWriter, r *http.Request) {
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
ok, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Services, operations.Delete)
if err != nil {
util.WriteErrorResponse("failed to validate permissions", http.StatusInternalServerError, w)
return
}
if !ok {
util.WriteErrorResponse("permission denied", http.StatusForbidden, w)
return
}
proxyID := mux.Vars(r)["proxyId"]
if proxyID == "" {
util.WriteErrorResponse("proxy ID is required", http.StatusBadRequest, w)
return
}
p, err := h.proxyMgr.GetAccountProxy(r.Context(), userAuth.AccountId)
if err != nil {
util.WriteErrorResponse("proxy not found", http.StatusNotFound, w)
return
}
if p.ID != proxyID {
util.WriteErrorResponse("proxy not found", http.StatusNotFound, w)
return
}
if h.disconnector != nil {
h.disconnector.ForceDisconnect(proxyID)
}
if err := h.proxyMgr.DeleteProxy(r.Context(), proxyID); err != nil {
util.WriteErrorResponse("failed to delete proxy", http.StatusInternalServerError, w)
return
}
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
}
func isNotFound(err error) bool {
e, ok := status.FromError(err)
return ok && e.Type() == status.NotFound
}
func toSelfHostedProxyResponse(p *proxy.Proxy, serviceCount int) api.SelfHostedProxy {
st := api.SelfHostedProxyStatus(p.Status)
resp := api.SelfHostedProxy{
Id: p.ID,
ClusterAddress: p.ClusterAddress,
Status: st,
LastSeen: p.LastSeen,
ServiceCount: serviceCount,
}
if p.IPAddress != "" {
resp.IpAddress = &p.IPAddress
}
if p.ConnectedAt != nil {
resp.ConnectedAt = p.ConnectedAt
}
return resp
}

View File

@@ -1,220 +0,0 @@
package selfhostedproxy
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/permissions/modules"
"github.com/netbirdio/netbird/management/server/permissions/operations"
"github.com/netbirdio/netbird/shared/auth"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/status"
)
type mockDisconnector struct {
disconnectedIDs []string
}
func (m *mockDisconnector) ForceDisconnect(proxyID string) {
m.disconnectedIDs = append(m.disconnectedIDs, proxyID)
}
func authContext(accountID, userID string) context.Context {
return nbcontext.SetUserAuthInContext(context.Background(), auth.UserAuth{
AccountId: accountID,
UserId: userID,
})
}
func TestListProxies_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
accountID := "acc-123"
now := time.Now()
connAt := now.Add(-1 * time.Hour)
proxyMgr := proxy.NewMockManager(ctrl)
proxyMgr.EXPECT().GetAccountProxy(gomock.Any(), accountID).Return(&proxy.Proxy{
ID: "proxy-1",
ClusterAddress: "byop.example.com",
IPAddress: "10.0.0.1",
AccountID: &accountID,
Status: proxy.StatusConnected,
LastSeen: now,
ConnectedAt: &connAt,
}, nil)
serviceMgr := rpservice.NewMockManager(ctrl)
serviceMgr.EXPECT().GetAccountServices(gomock.Any(), accountID).Return([]*rpservice.Service{
{ProxyCluster: "byop.example.com"},
{ProxyCluster: "byop.example.com"},
{ProxyCluster: "other.cluster.com"},
}, nil)
permsMgr := permissions.NewMockManager(ctrl)
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Read).Return(true, nil)
h := &handler{
proxyMgr: proxyMgr,
serviceMgr: serviceMgr,
permissionsManager: permsMgr,
}
req := httptest.NewRequest("GET", "/reverse-proxies/self-hosted-proxies", nil)
req = req.WithContext(authContext(accountID, "user-1"))
w := httptest.NewRecorder()
h.listProxies(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp []api.SelfHostedProxy
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
require.Len(t, resp, 1)
assert.Equal(t, "proxy-1", resp[0].Id)
assert.Equal(t, "byop.example.com", resp[0].ClusterAddress)
assert.Equal(t, 2, resp[0].ServiceCount)
assert.Equal(t, api.SelfHostedProxyStatus(proxy.StatusConnected), resp[0].Status)
}
func TestListProxies_NoProxy(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
proxyMgr := proxy.NewMockManager(ctrl)
proxyMgr.EXPECT().GetAccountProxy(gomock.Any(), "acc-123").Return(nil, status.Errorf(status.NotFound, "not found"))
permsMgr := permissions.NewMockManager(ctrl)
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Read).Return(true, nil)
h := &handler{
proxyMgr: proxyMgr,
permissionsManager: permsMgr,
}
req := httptest.NewRequest("GET", "/reverse-proxies/self-hosted-proxies", nil)
req = req.WithContext(authContext("acc-123", "user-1"))
w := httptest.NewRecorder()
h.listProxies(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp []api.SelfHostedProxy
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Empty(t, resp)
}
func TestListProxies_PermissionDenied(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
permsMgr := permissions.NewMockManager(ctrl)
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Read).Return(false, nil)
h := &handler{
permissionsManager: permsMgr,
}
req := httptest.NewRequest("GET", "/reverse-proxies/self-hosted-proxies", nil)
req = req.WithContext(authContext("acc-123", "user-1"))
w := httptest.NewRecorder()
h.listProxies(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestDeleteProxy_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
accountID := "acc-123"
disconnector := &mockDisconnector{}
proxyMgr := proxy.NewMockManager(ctrl)
proxyMgr.EXPECT().GetAccountProxy(gomock.Any(), accountID).Return(&proxy.Proxy{
ID: "proxy-1",
AccountID: &accountID,
Status: proxy.StatusConnected,
}, nil)
proxyMgr.EXPECT().DeleteProxy(gomock.Any(), "proxy-1").Return(nil)
permsMgr := permissions.NewMockManager(ctrl)
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Delete).Return(true, nil)
h := &handler{
proxyMgr: proxyMgr,
permissionsManager: permsMgr,
disconnector: disconnector,
}
req := httptest.NewRequest("DELETE", "/reverse-proxies/self-hosted-proxies/proxy-1", nil)
req = req.WithContext(authContext(accountID, "user-1"))
req = mux.SetURLVars(req, map[string]string{"proxyId": "proxy-1"})
w := httptest.NewRecorder()
h.deleteProxy(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, disconnector.disconnectedIDs, "proxy-1")
}
func TestDeleteProxy_WrongProxyID(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
accountID := "acc-123"
proxyMgr := proxy.NewMockManager(ctrl)
proxyMgr.EXPECT().GetAccountProxy(gomock.Any(), accountID).Return(&proxy.Proxy{
ID: "proxy-1",
AccountID: &accountID,
}, nil)
permsMgr := permissions.NewMockManager(ctrl)
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), accountID, "user-1", modules.Services, operations.Delete).Return(true, nil)
h := &handler{
proxyMgr: proxyMgr,
permissionsManager: permsMgr,
}
req := httptest.NewRequest("DELETE", "/reverse-proxies/self-hosted-proxies/proxy-other", nil)
req = req.WithContext(authContext(accountID, "user-1"))
req = mux.SetURLVars(req, map[string]string{"proxyId": "proxy-other"})
w := httptest.NewRecorder()
h.deleteProxy(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestDeleteProxy_PermissionDenied(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
permsMgr := permissions.NewMockManager(ctrl)
permsMgr.EXPECT().ValidateUserPermissions(gomock.Any(), "acc-123", "user-1", modules.Services, operations.Delete).Return(false, nil)
h := &handler{
permissionsManager: permsMgr,
}
req := httptest.NewRequest("DELETE", "/reverse-proxies/self-hosted-proxies/proxy-1", nil)
req = req.WithContext(authContext("acc-123", "user-1"))
req = mux.SetURLVars(req, map[string]string{"proxyId": "proxy-1"})
w := httptest.NewRecorder()
h.deleteProxy(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxytoken"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/selfhostedproxy"
reverseproxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
@@ -181,9 +180,6 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
}
proxytoken.RegisterEndpoints(accountManager.GetStore(), permissionsManager, router)
if proxyMgr != nil && serviceManager != nil {
selfhostedproxy.RegisterEndpoints(proxyMgr, serviceManager, permissionsManager, proxyGRPCServer, router)
}
// Register OAuth callback handler for proxy authentication
if proxyGRPCServer != nil {