diff --git a/management/internals/modules/reverseproxy/selfhostedproxy/handler.go b/management/internals/modules/reverseproxy/selfhostedproxy/handler.go deleted file mode 100644 index 9eb1e885f..000000000 --- a/management/internals/modules/reverseproxy/selfhostedproxy/handler.go +++ /dev/null @@ -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 -} diff --git a/management/internals/modules/reverseproxy/selfhostedproxy/handler_test.go b/management/internals/modules/reverseproxy/selfhostedproxy/handler_test.go deleted file mode 100644 index ea26233b1..000000000 --- a/management/internals/modules/reverseproxy/selfhostedproxy/handler_test.go +++ /dev/null @@ -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) -} diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 2674f77fd..d8a65fbc0 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -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 { diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 150f3af28..3aebac60c 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -3200,37 +3200,6 @@ components: example: "nbx_abc123..." required: - plain_token - SelfHostedProxy: - type: object - properties: - id: - type: string - description: Proxy instance ID - cluster_address: - type: string - description: Cluster domain or IP address - example: "proxy.example.com" - ip_address: - type: string - description: Proxy IP address - status: - type: string - enum: [ connected, disconnected ] - last_seen: - type: string - format: date-time - connected_at: - type: string - format: date-time - service_count: - type: integer - description: Number of services routed through this proxy's cluster - required: - - id - - cluster_address - - status - - last_seen - - service_count ProxyCluster: type: object description: A proxy cluster represents a group of proxy nodes serving the same address @@ -3243,9 +3212,14 @@ components: type: integer description: Number of proxy nodes connected in this cluster example: 3 + self_hosted: + type: boolean + description: Whether this cluster is a self-hosted (BYOP) proxy managed by the account owner + example: false required: - address - connected_proxies + - self_hosted ReverseProxyDomainType: type: string description: Type of Reverse Proxy Domain @@ -9773,55 +9747,6 @@ paths: "$ref": "#/components/responses/not_found" '500': "$ref": "#/components/responses/internal_error" - /api/reverse-proxies/self-hosted-proxies: - get: - summary: List Self-Hosted Proxies - description: Returns self-hosted proxies registered for the account - tags: [ Self-Hosted Proxies ] - security: - - BearerAuth: [ ] - - TokenAuth: [ ] - responses: - '200': - description: A JSON Array of self-hosted proxies - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/SelfHostedProxy' - '401': - "$ref": "#/components/responses/requires_authentication" - '403': - "$ref": "#/components/responses/forbidden" - '500': - "$ref": "#/components/responses/internal_error" - /api/reverse-proxies/self-hosted-proxies/{proxyId}: - delete: - summary: Delete a Self-Hosted Proxy - description: Remove a self-hosted proxy from the account - tags: [ Self-Hosted Proxies ] - security: - - BearerAuth: [ ] - - TokenAuth: [ ] - parameters: - - in: path - name: proxyId - required: true - schema: - type: string - description: The unique identifier of the proxy - responses: - '200': - description: Proxy deleted - '401': - "$ref": "#/components/responses/requires_authentication" - '403': - "$ref": "#/components/responses/forbidden" - '404': - "$ref": "#/components/responses/not_found" - '500': - "$ref": "#/components/responses/internal_error" /api/reverse-proxies/services: get: summary: List all Services @@ -9896,6 +9821,35 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/reverse-proxies/clusters/{clusterId}: + delete: + summary: Delete a self-hosted proxy cluster + description: Removes a self-hosted (BYOP) proxy cluster and disconnects it. Only self-hosted clusters can be deleted. + tags: [ Services ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: clusterId + required: true + schema: + type: string + description: The unique identifier of the proxy cluster + responses: + '200': + description: Proxy cluster deleted successfully + content: { } + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" /api/reverse-proxies/services/{serviceId}: get: summary: Retrieve a Service diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 45097f641..e7aff4a81 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -859,24 +859,6 @@ func (e ReverseProxyDomainType) Valid() bool { } } -// Defines values for SelfHostedProxyStatus. -const ( - SelfHostedProxyStatusConnected SelfHostedProxyStatus = "connected" - SelfHostedProxyStatusDisconnected SelfHostedProxyStatus = "disconnected" -) - -// Valid indicates whether the value is a known member of the SelfHostedProxyStatus enum. -func (e SelfHostedProxyStatus) Valid() bool { - switch e { - case SelfHostedProxyStatusConnected: - return true - case SelfHostedProxyStatusDisconnected: - return true - default: - return false - } -} - // Defines values for SentinelOneMatchAttributesNetworkStatus. const ( SentinelOneMatchAttributesNetworkStatusConnected SentinelOneMatchAttributesNetworkStatus = "connected" @@ -3308,6 +3290,9 @@ type ProxyCluster struct { // ConnectedProxies Number of proxy nodes connected in this cluster ConnectedProxies int `json:"connected_proxies"` + + // SelfHosted Whether this cluster is a self-hosted (BYOP) proxy managed by the account owner + SelfHosted bool `json:"self_hosted"` } // ProxyToken defines model for ProxyToken. @@ -3511,27 +3496,6 @@ type ScimTokenResponse struct { AuthToken string `json:"auth_token"` } -// SelfHostedProxy defines model for SelfHostedProxy. -type SelfHostedProxy struct { - // ClusterAddress Cluster domain or IP address - ClusterAddress string `json:"cluster_address"` - ConnectedAt *time.Time `json:"connected_at,omitempty"` - - // Id Proxy instance ID - Id string `json:"id"` - - // IpAddress Proxy IP address - IpAddress *string `json:"ip_address,omitempty"` - LastSeen time.Time `json:"last_seen"` - - // ServiceCount Number of services routed through this proxy's cluster - ServiceCount int `json:"service_count"` - Status SelfHostedProxyStatus `json:"status"` -} - -// SelfHostedProxyStatus defines model for SelfHostedProxy.Status. -type SelfHostedProxyStatus string - // SentinelOneMatchAttributes Attribute conditions to match when approving agents type SentinelOneMatchAttributes struct { // ActiveThreats The maximum allowed number of active threats on the agent