diff --git a/shared/management/client/rest/billing.go b/shared/management/client/rest/billing.go new file mode 100644 index 000000000..4ac9cdf55 --- /dev/null +++ b/shared/management/client/rest/billing.go @@ -0,0 +1,82 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// BillingAPI APIs for billing and invoices +type BillingAPI struct { + c *Client +} + +// GetUsage retrieves current usage statistics for the account +// See more: https://docs.netbird.io/api/resources/billing#get-current-usage +func (a *BillingAPI) GetUsage(ctx context.Context) (*api.UsageStats, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/usage", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.UsageStats](resp) + return &ret, err +} + +// GetSubscription retrieves the current subscription details +// See more: https://docs.netbird.io/api/resources/billing#get-current-subscription +func (a *BillingAPI) GetSubscription(ctx context.Context) (*api.Subscription, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/subscription", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.Subscription](resp) + return &ret, err +} + +// GetInvoices retrieves the account's paid invoices +// See more: https://docs.netbird.io/api/resources/billing#list-all-invoices +func (a *BillingAPI) GetInvoices(ctx context.Context) ([]api.InvoiceResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/invoices", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.InvoiceResponse](resp) + return ret, err +} + +// GetInvoicePDF retrieves the invoice PDF URL +// See more: https://docs.netbird.io/api/resources/billing#get-invoice-pdf +func (a *BillingAPI) GetInvoicePDF(ctx context.Context, invoiceID string) (*api.InvoicePDFResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/invoices/"+invoiceID+"/pdf", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.InvoicePDFResponse](resp) + return &ret, err +} + +// GetInvoiceCSV retrieves the invoice CSV content +// See more: https://docs.netbird.io/api/resources/billing#get-invoice-csv +func (a *BillingAPI) GetInvoiceCSV(ctx context.Context, invoiceID string) (string, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/invoices/"+invoiceID+"/csv", nil, nil) + if err != nil { + return "", err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[string](resp) + return ret, err +} diff --git a/shared/management/client/rest/billing_test.go b/shared/management/client/rest/billing_test.go new file mode 100644 index 000000000..060e459f6 --- /dev/null +++ b/shared/management/client/rest/billing_test.go @@ -0,0 +1,194 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var ( + testUsageStats = api.UsageStats{ + ActiveUsers: 15, + TotalUsers: 20, + ActivePeers: 10, + TotalPeers: 25, + } + + testSubscription = api.Subscription{ + Active: true, + PlanTier: "basic", + PriceId: "price_1HhxOp", + Currency: "USD", + Price: 1000, + Provider: "stripe", + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + } + + testInvoice = api.InvoiceResponse{ + Id: "inv_123", + PeriodStart: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + PeriodEnd: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), + Type: "invoice", + } + + testInvoicePDF = api.InvoicePDFResponse{ + Url: "https://example.com/invoice.pdf", + } +) + +func TestBilling_GetUsage_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/usage", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testUsageStats) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetUsage(context.Background()) + require.NoError(t, err) + assert.Equal(t, testUsageStats, *ret) + }) +} + +func TestBilling_GetUsage_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/usage", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetUsage(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestBilling_GetSubscription_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/subscription", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testSubscription) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetSubscription(context.Background()) + require.NoError(t, err) + assert.Equal(t, testSubscription, *ret) + }) +} + +func TestBilling_GetSubscription_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/subscription", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetSubscription(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestBilling_GetInvoices_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/invoices", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.InvoiceResponse{testInvoice}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetInvoices(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testInvoice, ret[0]) + }) +} + +func TestBilling_GetInvoices_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/invoices", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetInvoices(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestBilling_GetInvoicePDF_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/invoices/inv_123/pdf", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testInvoicePDF) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetInvoicePDF(context.Background(), "inv_123") + require.NoError(t, err) + assert.Equal(t, testInvoicePDF, *ret) + }) +} + +func TestBilling_GetInvoicePDF_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/invoices/inv_123/pdf", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetInvoicePDF(context.Background(), "inv_123") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestBilling_GetInvoiceCSV_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/invoices/inv_123/csv", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal("col1,col2\nval1,val2") + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetInvoiceCSV(context.Background(), "inv_123") + require.NoError(t, err) + assert.Equal(t, "col1,col2\nval1,val2", ret) + }) +} + +func TestBilling_GetInvoiceCSV_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/billing/invoices/inv_123/csv", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Billing.GetInvoiceCSV(context.Background(), "inv_123") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Empty(t, ret) + }) +} diff --git a/shared/management/client/rest/client.go b/shared/management/client/rest/client.go index ad8328093..99d8eb594 100644 --- a/shared/management/client/rest/client.go +++ b/shared/management/client/rest/client.go @@ -73,6 +73,38 @@ type Client struct { // Events NetBird Events APIs // see more: https://docs.netbird.io/api/resources/events Events *EventsAPI + + // Billing NetBird Billing APIs for subscriptions, plans, and invoices + // see more: https://docs.netbird.io/api/resources/billing + Billing *BillingAPI + + // MSP NetBird MSP tenant management APIs + // see more: https://docs.netbird.io/api/resources/msp + MSP *MSPAPI + + // EDR NetBird EDR integration APIs (Intune, SentinelOne, Falcon, Huntress) + // see more: https://docs.netbird.io/api/resources/edr + EDR *EDRAPI + + // SCIM NetBird SCIM IDP integration APIs + // see more: https://docs.netbird.io/api/resources/scim + SCIM *SCIMAPI + + // EventStreaming NetBird Event Streaming integration APIs + // see more: https://docs.netbird.io/api/resources/event-streaming + EventStreaming *EventStreamingAPI + + // IdentityProviders NetBird Identity Providers APIs + // see more: https://docs.netbird.io/api/resources/identity-providers + IdentityProviders *IdentityProvidersAPI + + // Ingress NetBird Ingress Peers APIs + // see more: https://docs.netbird.io/api/resources/ingress-ports + Ingress *IngressAPI + + // Instance NetBird Instance API + // see more: https://docs.netbird.io/api/resources/instance + Instance *InstanceAPI } // New initialize new Client instance using PAT token @@ -120,6 +152,14 @@ func (c *Client) initialize() { c.DNSZones = &DNSZonesAPI{c} c.GeoLocation = &GeoLocationAPI{c} c.Events = &EventsAPI{c} + c.Billing = &BillingAPI{c} + c.MSP = &MSPAPI{c} + c.EDR = &EDRAPI{c} + c.SCIM = &SCIMAPI{c} + c.EventStreaming = &EventStreamingAPI{c} + c.IdentityProviders = &IdentityProvidersAPI{c} + c.Ingress = &IngressAPI{c} + c.Instance = &InstanceAPI{c} } // NewRequest creates and executes new management API request diff --git a/shared/management/client/rest/edr.go b/shared/management/client/rest/edr.go new file mode 100644 index 000000000..7dfc891c2 --- /dev/null +++ b/shared/management/client/rest/edr.go @@ -0,0 +1,307 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// EDRAPI APIs for EDR integrations (Intune, SentinelOne, Falcon, Huntress) +type EDRAPI struct { + c *Client +} + +// GetIntuneIntegration retrieves the EDR Intune integration +// See more: https://docs.netbird.io/api/resources/edr#get-intune-integration +func (a *EDRAPI) GetIntuneIntegration(ctx context.Context) (*api.EDRIntuneResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/edr/intune", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRIntuneResponse](resp) + return &ret, err +} + +// CreateIntuneIntegration creates a new EDR Intune integration +// See more: https://docs.netbird.io/api/resources/edr#create-intune-integration +func (a *EDRAPI) CreateIntuneIntegration(ctx context.Context, request api.EDRIntuneRequest) (*api.EDRIntuneResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/edr/intune", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRIntuneResponse](resp) + return &ret, err +} + +// UpdateIntuneIntegration updates an existing EDR Intune integration +// See more: https://docs.netbird.io/api/resources/edr#update-intune-integration +func (a *EDRAPI) UpdateIntuneIntegration(ctx context.Context, request api.EDRIntuneRequest) (*api.EDRIntuneResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/edr/intune", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRIntuneResponse](resp) + return &ret, err +} + +// DeleteIntuneIntegration deletes the EDR Intune integration +// See more: https://docs.netbird.io/api/resources/edr#delete-intune-integration +func (a *EDRAPI) DeleteIntuneIntegration(ctx context.Context) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/edr/intune", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// GetSentinelOneIntegration retrieves the EDR SentinelOne integration +// See more: https://docs.netbird.io/api/resources/edr#get-sentinelone-integration +func (a *EDRAPI) GetSentinelOneIntegration(ctx context.Context) (*api.EDRSentinelOneResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/edr/sentinelone", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRSentinelOneResponse](resp) + return &ret, err +} + +// CreateSentinelOneIntegration creates a new EDR SentinelOne integration +// See more: https://docs.netbird.io/api/resources/edr#create-sentinelone-integration +func (a *EDRAPI) CreateSentinelOneIntegration(ctx context.Context, request api.EDRSentinelOneRequest) (*api.EDRSentinelOneResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/edr/sentinelone", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRSentinelOneResponse](resp) + return &ret, err +} + +// UpdateSentinelOneIntegration updates an existing EDR SentinelOne integration +// See more: https://docs.netbird.io/api/resources/edr#update-sentinelone-integration +func (a *EDRAPI) UpdateSentinelOneIntegration(ctx context.Context, request api.EDRSentinelOneRequest) (*api.EDRSentinelOneResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/edr/sentinelone", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRSentinelOneResponse](resp) + return &ret, err +} + +// DeleteSentinelOneIntegration deletes the EDR SentinelOne integration +// See more: https://docs.netbird.io/api/resources/edr#delete-sentinelone-integration +func (a *EDRAPI) DeleteSentinelOneIntegration(ctx context.Context) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/edr/sentinelone", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// GetFalconIntegration retrieves the EDR Falcon integration +// See more: https://docs.netbird.io/api/resources/edr#get-falcon-integration +func (a *EDRAPI) GetFalconIntegration(ctx context.Context) (*api.EDRFalconResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/edr/falcon", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRFalconResponse](resp) + return &ret, err +} + +// CreateFalconIntegration creates a new EDR Falcon integration +// See more: https://docs.netbird.io/api/resources/edr#create-falcon-integration +func (a *EDRAPI) CreateFalconIntegration(ctx context.Context, request api.EDRFalconRequest) (*api.EDRFalconResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/edr/falcon", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRFalconResponse](resp) + return &ret, err +} + +// UpdateFalconIntegration updates an existing EDR Falcon integration +// See more: https://docs.netbird.io/api/resources/edr#update-falcon-integration +func (a *EDRAPI) UpdateFalconIntegration(ctx context.Context, request api.EDRFalconRequest) (*api.EDRFalconResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/edr/falcon", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRFalconResponse](resp) + return &ret, err +} + +// DeleteFalconIntegration deletes the EDR Falcon integration +// See more: https://docs.netbird.io/api/resources/edr#delete-falcon-integration +func (a *EDRAPI) DeleteFalconIntegration(ctx context.Context) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/edr/falcon", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// GetHuntressIntegration retrieves the EDR Huntress integration +// See more: https://docs.netbird.io/api/resources/edr#get-huntress-integration +func (a *EDRAPI) GetHuntressIntegration(ctx context.Context) (*api.EDRHuntressResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/edr/huntress", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRHuntressResponse](resp) + return &ret, err +} + +// CreateHuntressIntegration creates a new EDR Huntress integration +// See more: https://docs.netbird.io/api/resources/edr#create-huntress-integration +func (a *EDRAPI) CreateHuntressIntegration(ctx context.Context, request api.EDRHuntressRequest) (*api.EDRHuntressResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/edr/huntress", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRHuntressResponse](resp) + return &ret, err +} + +// UpdateHuntressIntegration updates an existing EDR Huntress integration +// See more: https://docs.netbird.io/api/resources/edr#update-huntress-integration +func (a *EDRAPI) UpdateHuntressIntegration(ctx context.Context, request api.EDRHuntressRequest) (*api.EDRHuntressResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/edr/huntress", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.EDRHuntressResponse](resp) + return &ret, err +} + +// DeleteHuntressIntegration deletes the EDR Huntress integration +// See more: https://docs.netbird.io/api/resources/edr#delete-huntress-integration +func (a *EDRAPI) DeleteHuntressIntegration(ctx context.Context) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/edr/huntress", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// BypassPeerCompliance bypasses compliance for a non-compliant peer +// See more: https://docs.netbird.io/api/resources/edr#bypass-peer-compliance +func (a *EDRAPI) BypassPeerCompliance(ctx context.Context, peerID string) (*api.BypassResponse, error) { + resp, err := a.c.NewRequest(ctx, "POST", "/api/peers/"+peerID+"/edr/bypass", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.BypassResponse](resp) + return &ret, err +} + +// RevokePeerBypass revokes the compliance bypass for a peer +// See more: https://docs.netbird.io/api/resources/edr#revoke-peer-bypass +func (a *EDRAPI) RevokePeerBypass(ctx context.Context, peerID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+peerID+"/edr/bypass", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// ListBypassedPeers returns all peers that have compliance bypassed +// See more: https://docs.netbird.io/api/resources/edr#list-all-bypassed-peers +func (a *EDRAPI) ListBypassedPeers(ctx context.Context) ([]api.BypassResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/edr/bypassed", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.BypassResponse](resp) + return ret, err +} diff --git a/shared/management/client/rest/edr_test.go b/shared/management/client/rest/edr_test.go new file mode 100644 index 000000000..a2a48858c --- /dev/null +++ b/shared/management/client/rest/edr_test.go @@ -0,0 +1,422 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var ( + testIntuneResponse = api.EDRIntuneResponse{ + AccountId: "acc-1", + ClientId: "client-1", + TenantId: "tenant-1", + Enabled: true, + Id: 1, + Groups: []api.Group{}, + LastSyncedInterval: 24, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedBy: "user-1", + } + + testSentinelOneResponse = api.EDRSentinelOneResponse{ + AccountId: "acc-1", + ApiUrl: "https://sentinelone.example.com", + Enabled: true, + Id: 2, + Groups: []api.Group{}, + LastSyncedInterval: 24, + MatchAttributes: api.SentinelOneMatchAttributes{}, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedBy: "user-1", + } + + testFalconResponse = api.EDRFalconResponse{ + AccountId: "acc-1", + CloudId: "us-1", + Enabled: true, + Id: 3, + Groups: []api.Group{}, + ZtaScoreThreshold: 50, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedBy: "user-1", + } + + testHuntressResponse = api.EDRHuntressResponse{ + AccountId: "acc-1", + Enabled: true, + Id: 4, + Groups: []api.Group{}, + LastSyncedInterval: 24, + MatchAttributes: api.HuntressMatchAttributes{}, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedBy: "user-1", + } + + testBypassResponse = api.BypassResponse{ + PeerId: "peer-1", + } +) + +// Intune tests + +func TestEDR_GetIntuneIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/intune", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testIntuneResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.GetIntuneIntegration(context.Background()) + require.NoError(t, err) + assert.Equal(t, testIntuneResponse, *ret) + }) +} + +func TestEDR_GetIntuneIntegration_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/intune", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.GetIntuneIntegration(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestEDR_CreateIntuneIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/intune", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.EDRIntuneRequest + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "client-1", req.ClientId) + retBytes, _ := json.Marshal(testIntuneResponse) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.CreateIntuneIntegration(context.Background(), api.EDRIntuneRequest{ + ClientId: "client-1", + Secret: "secret", + TenantId: "tenant-1", + Groups: []string{"group-1"}, + LastSyncedInterval: 24, + }) + require.NoError(t, err) + assert.Equal(t, testIntuneResponse, *ret) + }) +} + +func TestEDR_CreateIntuneIntegration_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/intune", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.CreateIntuneIntegration(context.Background(), api.EDRIntuneRequest{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestEDR_UpdateIntuneIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/intune", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + retBytes, _ := json.Marshal(testIntuneResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.UpdateIntuneIntegration(context.Background(), api.EDRIntuneRequest{ + ClientId: "client-1", + Secret: "new-secret", + TenantId: "tenant-1", + Groups: []string{"group-1"}, + }) + require.NoError(t, err) + assert.Equal(t, testIntuneResponse, *ret) + }) +} + +func TestEDR_DeleteIntuneIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/intune", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.EDR.DeleteIntuneIntegration(context.Background()) + require.NoError(t, err) + }) +} + +func TestEDR_DeleteIntuneIntegration_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/intune", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.EDR.DeleteIntuneIntegration(context.Background()) + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +// SentinelOne tests + +func TestEDR_GetSentinelOneIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/sentinelone", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testSentinelOneResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.GetSentinelOneIntegration(context.Background()) + require.NoError(t, err) + assert.Equal(t, testSentinelOneResponse, *ret) + }) +} + +func TestEDR_CreateSentinelOneIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/sentinelone", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testSentinelOneResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.CreateSentinelOneIntegration(context.Background(), api.EDRSentinelOneRequest{ + ApiToken: "token", + ApiUrl: "https://sentinelone.example.com", + Groups: []string{"group-1"}, + LastSyncedInterval: 24, + MatchAttributes: api.SentinelOneMatchAttributes{}, + }) + require.NoError(t, err) + assert.Equal(t, testSentinelOneResponse, *ret) + }) +} + +func TestEDR_DeleteSentinelOneIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/sentinelone", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.EDR.DeleteSentinelOneIntegration(context.Background()) + require.NoError(t, err) + }) +} + +// Falcon tests + +func TestEDR_GetFalconIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/falcon", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testFalconResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.GetFalconIntegration(context.Background()) + require.NoError(t, err) + assert.Equal(t, testFalconResponse, *ret) + }) +} + +func TestEDR_CreateFalconIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/falcon", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testFalconResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.CreateFalconIntegration(context.Background(), api.EDRFalconRequest{ + ClientId: "client-1", + Secret: "secret", + CloudId: "us-1", + Groups: []string{"group-1"}, + ZtaScoreThreshold: 50, + }) + require.NoError(t, err) + assert.Equal(t, testFalconResponse, *ret) + }) +} + +func TestEDR_DeleteFalconIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/falcon", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.EDR.DeleteFalconIntegration(context.Background()) + require.NoError(t, err) + }) +} + +// Huntress tests + +func TestEDR_GetHuntressIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/huntress", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testHuntressResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.GetHuntressIntegration(context.Background()) + require.NoError(t, err) + assert.Equal(t, testHuntressResponse, *ret) + }) +} + +func TestEDR_CreateHuntressIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/huntress", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testHuntressResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.CreateHuntressIntegration(context.Background(), api.EDRHuntressRequest{ + ApiKey: "key", + ApiSecret: "secret", + Groups: []string{"group-1"}, + LastSyncedInterval: 24, + MatchAttributes: api.HuntressMatchAttributes{}, + }) + require.NoError(t, err) + assert.Equal(t, testHuntressResponse, *ret) + }) +} + +func TestEDR_DeleteHuntressIntegration_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/edr/huntress", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.EDR.DeleteHuntressIntegration(context.Background()) + require.NoError(t, err) + }) +} + +// Peer bypass tests + +func TestEDR_BypassPeerCompliance_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/peer-1/edr/bypass", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testBypassResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.BypassPeerCompliance(context.Background(), "peer-1") + require.NoError(t, err) + assert.Equal(t, testBypassResponse, *ret) + }) +} + +func TestEDR_BypassPeerCompliance_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/peer-1/edr/bypass", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Bad request", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.BypassPeerCompliance(context.Background(), "peer-1") + assert.Error(t, err) + assert.Equal(t, "Bad request", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestEDR_RevokePeerBypass_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/peer-1/edr/bypass", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.EDR.RevokePeerBypass(context.Background(), "peer-1") + require.NoError(t, err) + }) +} + +func TestEDR_RevokePeerBypass_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/peer-1/edr/bypass", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.EDR.RevokePeerBypass(context.Background(), "peer-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestEDR_ListBypassedPeers_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/edr/bypassed", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.BypassResponse{testBypassResponse}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.ListBypassedPeers(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testBypassResponse, ret[0]) + }) +} + +func TestEDR_ListBypassedPeers_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/edr/bypassed", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EDR.ListBypassedPeers(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} diff --git a/shared/management/client/rest/event_streaming.go b/shared/management/client/rest/event_streaming.go new file mode 100644 index 000000000..99a02bd33 --- /dev/null +++ b/shared/management/client/rest/event_streaming.go @@ -0,0 +1,92 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + "strconv" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// EventStreamingAPI APIs for event streaming integrations +type EventStreamingAPI struct { + c *Client +} + +// List retrieves all event streaming integrations +// See more: https://docs.netbird.io/api/resources/event-streaming#list-all-event-streaming-integrations +func (a *EventStreamingAPI) List(ctx context.Context) ([]api.IntegrationResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/event-streaming", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.IntegrationResponse](resp) + return ret, err +} + +// Get retrieves a specific event streaming integration by ID +// See more: https://docs.netbird.io/api/resources/event-streaming#retrieve-an-event-streaming-integration +func (a *EventStreamingAPI) Get(ctx context.Context, integrationID int) (*api.IntegrationResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/event-streaming/"+strconv.Itoa(integrationID), nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IntegrationResponse](resp) + return &ret, err +} + +// Create creates a new event streaming integration +// See more: https://docs.netbird.io/api/resources/event-streaming#create-an-event-streaming-integration +func (a *EventStreamingAPI) Create(ctx context.Context, request api.CreateIntegrationRequest) (*api.IntegrationResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/event-streaming", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IntegrationResponse](resp) + return &ret, err +} + +// Update updates an existing event streaming integration +// See more: https://docs.netbird.io/api/resources/event-streaming#update-an-event-streaming-integration +func (a *EventStreamingAPI) Update(ctx context.Context, integrationID int, request api.CreateIntegrationRequest) (*api.IntegrationResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/event-streaming/"+strconv.Itoa(integrationID), bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IntegrationResponse](resp) + return &ret, err +} + +// Delete deletes an event streaming integration +// See more: https://docs.netbird.io/api/resources/event-streaming#delete-an-event-streaming-integration +func (a *EventStreamingAPI) Delete(ctx context.Context, integrationID int) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/event-streaming/"+strconv.Itoa(integrationID), nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} diff --git a/shared/management/client/rest/event_streaming_test.go b/shared/management/client/rest/event_streaming_test.go new file mode 100644 index 000000000..eebe291e4 --- /dev/null +++ b/shared/management/client/rest/event_streaming_test.go @@ -0,0 +1,194 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var ( + testIntegrationResponse = api.IntegrationResponse{ + Id: ptr[int64](1), + AccountId: ptr("acc-1"), + Platform: (*api.IntegrationResponsePlatform)(ptr("datadog")), + Enabled: ptr(true), + Config: &map[string]string{"api_key": "****"}, + CreatedAt: ptr(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + UpdatedAt: ptr(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + } +) + +func TestEventStreaming_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.IntegrationResponse{testIntegrationResponse}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testIntegrationResponse, ret[0]) + }) +} + +func TestEventStreaming_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestEventStreaming_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming/1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testIntegrationResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.Get(context.Background(), 1) + require.NoError(t, err) + assert.Equal(t, testIntegrationResponse, *ret) + }) +} + +func TestEventStreaming_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming/1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.Get(context.Background(), 1) + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestEventStreaming_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.CreateIntegrationRequest + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, api.CreateIntegrationRequestPlatformDatadog, req.Platform) + assert.Equal(t, true, req.Enabled) + retBytes, _ := json.Marshal(testIntegrationResponse) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.Create(context.Background(), api.CreateIntegrationRequest{ + Platform: api.CreateIntegrationRequestPlatformDatadog, + Enabled: true, + Config: map[string]string{"api_key": "test-key"}, + }) + require.NoError(t, err) + assert.Equal(t, testIntegrationResponse, *ret) + }) +} + +func TestEventStreaming_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.Create(context.Background(), api.CreateIntegrationRequest{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestEventStreaming_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming/1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.CreateIntegrationRequest + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, false, req.Enabled) + retBytes, _ := json.Marshal(testIntegrationResponse) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.Update(context.Background(), 1, api.CreateIntegrationRequest{ + Platform: api.CreateIntegrationRequestPlatformDatadog, + Enabled: false, + Config: map[string]string{"api_key": "updated-key"}, + }) + require.NoError(t, err) + assert.Equal(t, testIntegrationResponse, *ret) + }) +} + +func TestEventStreaming_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming/1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.EventStreaming.Update(context.Background(), 1, api.CreateIntegrationRequest{}) + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestEventStreaming_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming/1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.EventStreaming.Delete(context.Background(), 1) + require.NoError(t, err) + }) +} + +func TestEventStreaming_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/event-streaming/1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.EventStreaming.Delete(context.Background(), 1) + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} diff --git a/shared/management/client/rest/events.go b/shared/management/client/rest/events.go index 2d25333ae..348d0698a 100644 --- a/shared/management/client/rest/events.go +++ b/shared/management/client/rest/events.go @@ -2,6 +2,8 @@ package rest import ( "context" + "fmt" + "time" "github.com/netbirdio/netbird/shared/management/http/api" ) @@ -11,10 +13,79 @@ type EventsAPI struct { c *Client } -// List list all events -// See more: https://docs.netbird.io/api/resources/events#list-all-events -func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) { - resp, err := a.c.NewRequest(ctx, "GET", "/api/events", nil, nil) +// NetworkTrafficOption options for ListNetworkTrafficEvents API +type NetworkTrafficOption func(query map[string]string) + +func NetworkTrafficPage(page int) NetworkTrafficOption { + return func(query map[string]string) { + query["page"] = fmt.Sprintf("%d", page) + } +} + +func NetworkTrafficPageSize(pageSize int) NetworkTrafficOption { + return func(query map[string]string) { + query["page_size"] = fmt.Sprintf("%d", pageSize) + } +} + +func NetworkTrafficUserID(userID string) NetworkTrafficOption { + return func(query map[string]string) { + query["user_id"] = userID + } +} + +func NetworkTrafficReporterID(reporterID string) NetworkTrafficOption { + return func(query map[string]string) { + query["reporter_id"] = reporterID + } +} + +func NetworkTrafficProtocol(protocol int) NetworkTrafficOption { + return func(query map[string]string) { + query["protocol"] = fmt.Sprintf("%d", protocol) + } +} + +func NetworkTrafficType(t api.GetApiEventsNetworkTrafficParamsType) NetworkTrafficOption { + return func(query map[string]string) { + query["type"] = string(t) + } +} + +func NetworkTrafficConnectionType(ct api.GetApiEventsNetworkTrafficParamsConnectionType) NetworkTrafficOption { + return func(query map[string]string) { + query["connection_type"] = string(ct) + } +} + +func NetworkTrafficDirection(d api.GetApiEventsNetworkTrafficParamsDirection) NetworkTrafficOption { + return func(query map[string]string) { + query["direction"] = string(d) + } +} + +func NetworkTrafficSearch(search string) NetworkTrafficOption { + return func(query map[string]string) { + query["search"] = search + } +} + +func NetworkTrafficStartDate(t time.Time) NetworkTrafficOption { + return func(query map[string]string) { + query["start_date"] = t.Format(time.RFC3339) + } +} + +func NetworkTrafficEndDate(t time.Time) NetworkTrafficOption { + return func(query map[string]string) { + query["end_date"] = t.Format(time.RFC3339) + } +} + +// ListAuditEvents list all audit events +// See more: https://docs.netbird.io/api/resources/events#list-all-audit-events +func (a *EventsAPI) ListAuditEvents(ctx context.Context) ([]api.Event, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/events/audit", nil, nil) if err != nil { return nil, err } @@ -24,3 +95,21 @@ func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) { ret, err := parseResponse[[]api.Event](resp) return ret, err } + +// ListNetworkTrafficEvents list network traffic events +// See more: https://docs.netbird.io/api/resources/events#list-network-traffic-events +func (a *EventsAPI) ListNetworkTrafficEvents(ctx context.Context, opts ...NetworkTrafficOption) (*api.NetworkTrafficEventsResponse, error) { + query := make(map[string]string) + for _, o := range opts { + o(query) + } + resp, err := a.c.NewRequest(ctx, "GET", "/api/events/network-traffic", nil, query) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.NetworkTrafficEventsResponse](resp) + return &ret, err +} diff --git a/shared/management/client/rest/events_test.go b/shared/management/client/rest/events_test.go index 1ee10eb6e..d4bdae15d 100644 --- a/shared/management/client/rest/events_test.go +++ b/shared/management/client/rest/events_test.go @@ -21,37 +21,76 @@ var ( Activity: "AccountCreate", ActivityCode: api.EventActivityCodeAccountCreate, } + + testNetworkTrafficResponse = api.NetworkTrafficEventsResponse{ + Data: []api.NetworkTrafficEvent{}, + Page: 1, + PageSize: 50, + } ) -func TestEvents_List_200(t *testing.T) { +func TestEvents_ListAuditEvents_200(t *testing.T) { withMockClient(func(c *rest.Client, mux *http.ServeMux) { - mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/events/audit", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Event{testEvent}) _, err := w.Write(retBytes) require.NoError(t, err) }) - ret, err := c.Events.List(context.Background()) + ret, err := c.Events.ListAuditEvents(context.Background()) require.NoError(t, err) assert.Len(t, ret, 1) assert.Equal(t, testEvent, ret[0]) }) } -func TestEvents_List_Err(t *testing.T) { +func TestEvents_ListAuditEvents_Err(t *testing.T) { withMockClient(func(c *rest.Client, mux *http.ServeMux) { - mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/events/audit", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) _, err := w.Write(retBytes) require.NoError(t, err) }) - ret, err := c.Events.List(context.Background()) + ret, err := c.Events.ListAuditEvents(context.Background()) assert.Error(t, err) assert.Equal(t, "No", err.Error()) assert.Empty(t, ret) }) } +func TestEvents_ListNetworkTrafficEvents_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events/network-traffic", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "1", r.URL.Query().Get("page")) + assert.Equal(t, "50", r.URL.Query().Get("page_size")) + retBytes, _ := json.Marshal(testNetworkTrafficResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.ListNetworkTrafficEvents(context.Background(), + rest.NetworkTrafficPage(1), + rest.NetworkTrafficPageSize(50), + ) + require.NoError(t, err) + assert.Equal(t, testNetworkTrafficResponse, *ret) + }) +} + +func TestEvents_ListNetworkTrafficEvents_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events/network-traffic", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.ListNetworkTrafficEvents(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + func TestEvents_Integration(t *testing.T) { withBlackBoxServer(t, func(c *rest.Client) { // Do something that would trigger any event @@ -62,7 +101,7 @@ func TestEvents_Integration(t *testing.T) { }) require.NoError(t, err) - events, err := c.Events.List(context.Background()) + events, err := c.Events.ListAuditEvents(context.Background()) require.NoError(t, err) assert.NotEmpty(t, events) }) diff --git a/shared/management/client/rest/identity_providers.go b/shared/management/client/rest/identity_providers.go new file mode 100644 index 000000000..2a725183d --- /dev/null +++ b/shared/management/client/rest/identity_providers.go @@ -0,0 +1,92 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// IdentityProvidersAPI APIs for Identity Providers, do not use directly +type IdentityProvidersAPI struct { + c *Client +} + +// List all identity providers +// See more: https://docs.netbird.io/api/resources/identity-providers#list-all-identity-providers +func (a *IdentityProvidersAPI) List(ctx context.Context) ([]api.IdentityProvider, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/identity-providers", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.IdentityProvider](resp) + return ret, err +} + +// Get identity provider info +// See more: https://docs.netbird.io/api/resources/identity-providers#retrieve-an-identity-provider +func (a *IdentityProvidersAPI) Get(ctx context.Context, idpID string) (*api.IdentityProvider, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/identity-providers/"+idpID, nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IdentityProvider](resp) + return &ret, err +} + +// Create new identity provider +// See more: https://docs.netbird.io/api/resources/identity-providers#create-an-identity-provider +func (a *IdentityProvidersAPI) Create(ctx context.Context, request api.PostApiIdentityProvidersJSONRequestBody) (*api.IdentityProvider, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/identity-providers", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IdentityProvider](resp) + return &ret, err +} + +// Update update identity provider +// See more: https://docs.netbird.io/api/resources/identity-providers#update-an-identity-provider +func (a *IdentityProvidersAPI) Update(ctx context.Context, idpID string, request api.PutApiIdentityProvidersIdpIdJSONRequestBody) (*api.IdentityProvider, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/identity-providers/"+idpID, bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IdentityProvider](resp) + return &ret, err +} + +// Delete delete identity provider +// See more: https://docs.netbird.io/api/resources/identity-providers#delete-an-identity-provider +func (a *IdentityProvidersAPI) Delete(ctx context.Context, idpID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/identity-providers/"+idpID, nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} diff --git a/shared/management/client/rest/identity_providers_test.go b/shared/management/client/rest/identity_providers_test.go new file mode 100644 index 000000000..e6edab549 --- /dev/null +++ b/shared/management/client/rest/identity_providers_test.go @@ -0,0 +1,183 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var testIdentityProvider = api.IdentityProvider{ + ClientId: "test-client-id", + Id: ptr("Test"), +} + +func TestIdentityProviders_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.IdentityProvider{testIdentityProvider}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testIdentityProvider, ret[0]) + }) +} + +func TestIdentityProviders_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestIdentityProviders_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testIdentityProvider) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testIdentityProvider, *ret) + }) +} + +func TestIdentityProviders_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestIdentityProviders_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiIdentityProvidersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "new-client-id", req.ClientId) + retBytes, _ := json.Marshal(testIdentityProvider) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.Create(context.Background(), api.PostApiIdentityProvidersJSONRequestBody{ + ClientId: "new-client-id", + }) + require.NoError(t, err) + assert.Equal(t, testIdentityProvider, *ret) + }) +} + +func TestIdentityProviders_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.Create(context.Background(), api.PostApiIdentityProvidersJSONRequestBody{ + ClientId: "new-client-id", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestIdentityProviders_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiIdentityProvidersIdpIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "updated-client-id", req.ClientId) + retBytes, _ := json.Marshal(testIdentityProvider) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.Update(context.Background(), "Test", api.PutApiIdentityProvidersIdpIdJSONRequestBody{ + ClientId: "updated-client-id", + }) + require.NoError(t, err) + assert.Equal(t, testIdentityProvider, *ret) + }) +} + +func TestIdentityProviders_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.IdentityProviders.Update(context.Background(), "Test", api.PutApiIdentityProvidersIdpIdJSONRequestBody{ + ClientId: "updated-client-id", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestIdentityProviders_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.IdentityProviders.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestIdentityProviders_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/identity-providers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.IdentityProviders.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} diff --git a/shared/management/client/rest/ingress.go b/shared/management/client/rest/ingress.go new file mode 100644 index 000000000..f69288d7e --- /dev/null +++ b/shared/management/client/rest/ingress.go @@ -0,0 +1,92 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// IngressAPI APIs for Ingress Peers, do not use directly +type IngressAPI struct { + c *Client +} + +// List all ingress peers +// See more: https://docs.netbird.io/api/resources/ingress#list-all-ingress-peers +func (a *IngressAPI) List(ctx context.Context) ([]api.IngressPeer, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/ingress/peers", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.IngressPeer](resp) + return ret, err +} + +// Get ingress peer info +// See more: https://docs.netbird.io/api/resources/ingress#retrieve-an-ingress-peer +func (a *IngressAPI) Get(ctx context.Context, ingressPeerID string) (*api.IngressPeer, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/ingress/peers/"+ingressPeerID, nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IngressPeer](resp) + return &ret, err +} + +// Create new ingress peer +// See more: https://docs.netbird.io/api/resources/ingress#create-an-ingress-peer +func (a *IngressAPI) Create(ctx context.Context, request api.PostApiIngressPeersJSONRequestBody) (*api.IngressPeer, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/ingress/peers", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IngressPeer](resp) + return &ret, err +} + +// Update update ingress peer +// See more: https://docs.netbird.io/api/resources/ingress#update-an-ingress-peer +func (a *IngressAPI) Update(ctx context.Context, ingressPeerID string, request api.PutApiIngressPeersIngressPeerIdJSONRequestBody) (*api.IngressPeer, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/ingress/peers/"+ingressPeerID, bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IngressPeer](resp) + return &ret, err +} + +// Delete delete ingress peer +// See more: https://docs.netbird.io/api/resources/ingress#delete-an-ingress-peer +func (a *IngressAPI) Delete(ctx context.Context, ingressPeerID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/ingress/peers/"+ingressPeerID, nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} diff --git a/shared/management/client/rest/ingress_test.go b/shared/management/client/rest/ingress_test.go new file mode 100644 index 000000000..c915db094 --- /dev/null +++ b/shared/management/client/rest/ingress_test.go @@ -0,0 +1,184 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var testIngressPeer = api.IngressPeer{ + Connected: true, + Enabled: true, + Id: "Test", +} + +func TestIngress_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.IngressPeer{testIngressPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testIngressPeer, ret[0]) + }) +} + +func TestIngress_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestIngress_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testIngressPeer) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testIngressPeer, *ret) + }) +} + +func TestIngress_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestIngress_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiIngressPeersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "peer-id", req.PeerId) + retBytes, _ := json.Marshal(testIngressPeer) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.Create(context.Background(), api.PostApiIngressPeersJSONRequestBody{ + PeerId: "peer-id", + }) + require.NoError(t, err) + assert.Equal(t, testIngressPeer, *ret) + }) +} + +func TestIngress_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.Create(context.Background(), api.PostApiIngressPeersJSONRequestBody{ + PeerId: "peer-id", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestIngress_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiIngressPeersIngressPeerIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.Enabled) + retBytes, _ := json.Marshal(testIngressPeer) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.Update(context.Background(), "Test", api.PutApiIngressPeersIngressPeerIdJSONRequestBody{ + Enabled: true, + }) + require.NoError(t, err) + assert.Equal(t, testIngressPeer, *ret) + }) +} + +func TestIngress_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Ingress.Update(context.Background(), "Test", api.PutApiIngressPeersIngressPeerIdJSONRequestBody{ + Enabled: true, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestIngress_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Ingress.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestIngress_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Ingress.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} diff --git a/shared/management/client/rest/instance.go b/shared/management/client/rest/instance.go new file mode 100644 index 000000000..041879b41 --- /dev/null +++ b/shared/management/client/rest/instance.go @@ -0,0 +1,46 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// InstanceAPI APIs for Instance status and version, do not use directly +type InstanceAPI struct { + c *Client +} + +// GetStatus get instance status +// See more: https://docs.netbird.io/api/resources/instance#get-instance-status +func (a *InstanceAPI) GetStatus(ctx context.Context) (*api.InstanceStatus, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/instance", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.InstanceStatus](resp) + return &ret, err +} + +// Setup perform initial instance setup +// See more: https://docs.netbird.io/api/resources/instance#setup-instance +func (a *InstanceAPI) Setup(ctx context.Context, request api.PostApiSetupJSONRequestBody) (*api.SetupResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/setup", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.SetupResponse](resp) + return &ret, err +} diff --git a/shared/management/client/rest/instance_test.go b/shared/management/client/rest/instance_test.go new file mode 100644 index 000000000..52125838d --- /dev/null +++ b/shared/management/client/rest/instance_test.go @@ -0,0 +1,96 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var ( + testInstanceStatus = api.InstanceStatus{ + SetupRequired: true, + } + + testSetupResponse = api.SetupResponse{ + Email: "admin@example.com", + UserId: "user-123", + } +) + +func TestInstance_GetStatus_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/instance", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testInstanceStatus) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Instance.GetStatus(context.Background()) + require.NoError(t, err) + assert.Equal(t, testInstanceStatus, *ret) + }) +} + +func TestInstance_GetStatus_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/instance", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Instance.GetStatus(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestInstance_Setup_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiSetupJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "admin@example.com", req.Email) + retBytes, _ := json.Marshal(testSetupResponse) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Instance.Setup(context.Background(), api.PostApiSetupJSONRequestBody{ + Email: "admin@example.com", + }) + require.NoError(t, err) + assert.Equal(t, testSetupResponse, *ret) + }) +} + +func TestInstance_Setup_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Instance.Setup(context.Background(), api.PostApiSetupJSONRequestBody{ + Email: "admin@example.com", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} diff --git a/shared/management/client/rest/msp.go b/shared/management/client/rest/msp.go new file mode 100644 index 000000000..d820ccbde --- /dev/null +++ b/shared/management/client/rest/msp.go @@ -0,0 +1,122 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// MSPAPI APIs for MSP tenant management +type MSPAPI struct { + c *Client +} + +// ListTenants retrieves all MSP tenants +// See more: https://docs.netbird.io/api/resources/msp#list-all-tenants +func (a *MSPAPI) ListTenants(ctx context.Context) (*api.GetTenantsResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/msp/tenants", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.GetTenantsResponse](resp) + return &ret, err +} + +// CreateTenant creates a new MSP tenant +// See more: https://docs.netbird.io/api/resources/msp#create-a-tenant +func (a *MSPAPI) CreateTenant(ctx context.Context, request api.CreateTenantRequest) (*api.TenantResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/msp/tenants", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.TenantResponse](resp) + return &ret, err +} + +// UpdateTenant updates an existing MSP tenant +// See more: https://docs.netbird.io/api/resources/msp#update-a-tenant +func (a *MSPAPI) UpdateTenant(ctx context.Context, tenantID string, request api.UpdateTenantRequest) (*api.TenantResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/msp/tenants/"+tenantID, bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.TenantResponse](resp) + return &ret, err +} + +// DeleteTenant deletes an MSP tenant +// See more: https://docs.netbird.io/api/resources/msp#delete-a-tenant +func (a *MSPAPI) DeleteTenant(ctx context.Context, tenantID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/msp/tenants/"+tenantID, nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// UnlinkTenant unlinks a tenant from the MSP account +// See more: https://docs.netbird.io/api/resources/msp#unlink-a-tenant +func (a *MSPAPI) UnlinkTenant(ctx context.Context, tenantID, owner string) error { + params := map[string]string{"owner": owner} + requestBytes, err := json.Marshal(params) + if err != nil { + return err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/msp/tenants/"+tenantID+"/unlink", bytes.NewReader(requestBytes), nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// VerifyTenantDNS verifies a tenant domain DNS challenge +// See more: https://docs.netbird.io/api/resources/msp#verify-tenant-dns +func (a *MSPAPI) VerifyTenantDNS(ctx context.Context, tenantID string) error { + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/msp/tenants/"+tenantID+"/dns", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// InviteTenant invites an existing account as a tenant to the MSP account +// See more: https://docs.netbird.io/api/resources/msp#invite-a-tenant +func (a *MSPAPI) InviteTenant(ctx context.Context, tenantID string) (*api.TenantResponse, error) { + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/msp/tenants/"+tenantID+"/invite", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.TenantResponse](resp) + return &ret, err +} diff --git a/shared/management/client/rest/msp_test.go b/shared/management/client/rest/msp_test.go new file mode 100644 index 000000000..7078346f3 --- /dev/null +++ b/shared/management/client/rest/msp_test.go @@ -0,0 +1,251 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var ( + testTenant = api.TenantResponse{ + Id: "tenant-1", + Name: "Test Tenant", + Domain: "test.example.com", + DnsChallenge: "challenge-123", + Status: "active", + Groups: []api.TenantGroupResponse{}, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + } +) + +func TestMSP_ListTenants_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.TenantResponse{testTenant}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.ListTenants(context.Background()) + require.NoError(t, err) + assert.Len(t, *ret, 1) + assert.Equal(t, testTenant, (*ret)[0]) + }) +} + +func TestMSP_ListTenants_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.ListTenants(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestMSP_CreateTenant_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.CreateTenantRequest + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "Test Tenant", req.Name) + assert.Equal(t, "test.example.com", req.Domain) + retBytes, _ := json.Marshal(testTenant) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.CreateTenant(context.Background(), api.CreateTenantRequest{ + Name: "Test Tenant", + Domain: "test.example.com", + Groups: []api.TenantGroupResponse{}, + }) + require.NoError(t, err) + assert.Equal(t, testTenant, *ret) + }) +} + +func TestMSP_CreateTenant_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.CreateTenant(context.Background(), api.CreateTenantRequest{ + Name: "Test Tenant", + Domain: "test.example.com", + Groups: []api.TenantGroupResponse{}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestMSP_UpdateTenant_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.UpdateTenantRequest + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "Updated Tenant", req.Name) + retBytes, _ := json.Marshal(testTenant) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.UpdateTenant(context.Background(), "tenant-1", api.UpdateTenantRequest{ + Name: "Updated Tenant", + Groups: []api.TenantGroupResponse{}, + }) + require.NoError(t, err) + assert.Equal(t, testTenant, *ret) + }) +} + +func TestMSP_UpdateTenant_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.UpdateTenant(context.Background(), "tenant-1", api.UpdateTenantRequest{ + Name: "Updated Tenant", + Groups: []api.TenantGroupResponse{}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestMSP_DeleteTenant_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.MSP.DeleteTenant(context.Background(), "tenant-1") + require.NoError(t, err) + }) +} + +func TestMSP_DeleteTenant_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.MSP.DeleteTenant(context.Background(), "tenant-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestMSP_UnlinkTenant_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1/unlink", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + w.WriteHeader(200) + }) + err := c.MSP.UnlinkTenant(context.Background(), "tenant-1", "owner-1") + require.NoError(t, err) + }) +} + +func TestMSP_UnlinkTenant_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1/unlink", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.MSP.UnlinkTenant(context.Background(), "tenant-1", "owner-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestMSP_VerifyTenantDNS_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1/dns", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + w.WriteHeader(200) + }) + err := c.MSP.VerifyTenantDNS(context.Background(), "tenant-1") + require.NoError(t, err) + }) +} + +func TestMSP_VerifyTenantDNS_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1/dns", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Failed", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.MSP.VerifyTenantDNS(context.Background(), "tenant-1") + assert.Error(t, err) + assert.Equal(t, "Failed", err.Error()) + }) +} + +func TestMSP_InviteTenant_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1/invite", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testTenant) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.InviteTenant(context.Background(), "tenant-1") + require.NoError(t, err) + assert.Equal(t, testTenant, *ret) + }) +} + +func TestMSP_InviteTenant_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/msp/tenants/tenant-1/invite", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.MSP.InviteTenant(context.Background(), "tenant-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Nil(t, ret) + }) +} diff --git a/shared/management/client/rest/networks.go b/shared/management/client/rest/networks.go index cb25dcbef..86dd20c7b 100644 --- a/shared/management/client/rest/networks.go +++ b/shared/management/client/rest/networks.go @@ -91,6 +91,20 @@ func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error { return nil } +// ListAllRouters list all routers across all networks +// See more: https://docs.netbird.io/api/resources/networks#list-all-network-routers +func (a *NetworksAPI) ListAllRouters(ctx context.Context) ([]api.NetworkRouter, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/routers", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.NetworkRouter](resp) + return ret, err +} + // NetworkResourcesAPI APIs for Network Resources, do not use directly type NetworkResourcesAPI struct { c *Client diff --git a/shared/management/client/rest/networks_test.go b/shared/management/client/rest/networks_test.go index 2bf1a0d3b..33c9e72bb 100644 --- a/shared/management/client/rest/networks_test.go +++ b/shared/management/client/rest/networks_test.go @@ -219,6 +219,35 @@ func TestNetworks_Integration(t *testing.T) { }) } +func TestNetworks_ListAllRouters_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NetworkRouter{testNetworkRouter}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.ListAllRouters(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetworkRouter, ret[0]) + }) +} + +func TestNetworks_ListAllRouters_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.ListAllRouters(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + func TestNetworkResources_List_200(t *testing.T) { withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { diff --git a/shared/management/client/rest/peers.go b/shared/management/client/rest/peers.go index 359c21e42..b22bcae67 100644 --- a/shared/management/client/rest/peers.go +++ b/shared/management/client/rest/peers.go @@ -106,3 +106,173 @@ func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]ap ret, err := parseResponse[[]api.Peer](resp) return ret, err } + +// CreateTemporaryAccess create temporary access for a peer +// See more: https://docs.netbird.io/api/resources/peers#create-temporary-access +func (a *PeersAPI) CreateTemporaryAccess(ctx context.Context, peerID string, request api.PostApiPeersPeerIdTemporaryAccessJSONRequestBody) (*api.PeerTemporaryAccessResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/peers/"+peerID+"/temporary-access", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.PeerTemporaryAccessResponse](resp) + return &ret, err +} + +// PeerIngressPortsAPI APIs for Peer Ingress Ports, do not use directly +type PeerIngressPortsAPI struct { + c *Client + peerID string +} + +// IngressPorts APIs for peer ingress ports +func (a *PeersAPI) IngressPorts(peerID string) *PeerIngressPortsAPI { + return &PeerIngressPortsAPI{ + c: a.c, + peerID: peerID, + } +} + +// List list all ingress port allocations for a peer +// See more: https://docs.netbird.io/api/resources/peers#list-all-ingress-port-allocations +func (a *PeerIngressPortsAPI) List(ctx context.Context) ([]api.IngressPortAllocation, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/ingress/ports", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.IngressPortAllocation](resp) + return ret, err +} + +// Get get ingress port allocation info +// See more: https://docs.netbird.io/api/resources/peers#retrieve-an-ingress-port-allocation +func (a *PeerIngressPortsAPI) Get(ctx context.Context, allocationID string) (*api.IngressPortAllocation, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IngressPortAllocation](resp) + return &ret, err +} + +// Create create new ingress port allocation +// See more: https://docs.netbird.io/api/resources/peers#create-an-ingress-port-allocation +func (a *PeerIngressPortsAPI) Create(ctx context.Context, request api.PostApiPeersPeerIdIngressPortsJSONRequestBody) (*api.IngressPortAllocation, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/peers/"+a.peerID+"/ingress/ports", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IngressPortAllocation](resp) + return &ret, err +} + +// Update update ingress port allocation +// See more: https://docs.netbird.io/api/resources/peers#update-an-ingress-port-allocation +func (a *PeerIngressPortsAPI) Update(ctx context.Context, allocationID string, request api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody) (*api.IngressPortAllocation, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.IngressPortAllocation](resp) + return &ret, err +} + +// Delete delete ingress port allocation +// See more: https://docs.netbird.io/api/resources/peers#delete-an-ingress-port-allocation +func (a *PeerIngressPortsAPI) Delete(ctx context.Context, allocationID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} + +// PeerJobsAPI APIs for Peer Jobs, do not use directly +type PeerJobsAPI struct { + c *Client + peerID string +} + +// Jobs APIs for peer jobs +func (a *PeersAPI) Jobs(peerID string) *PeerJobsAPI { + return &PeerJobsAPI{ + c: a.c, + peerID: peerID, + } +} + +// List list all jobs for a peer +// See more: https://docs.netbird.io/api/resources/peers#list-all-peer-jobs +func (a *PeerJobsAPI) List(ctx context.Context) ([]api.JobResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/jobs", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.JobResponse](resp) + return ret, err +} + +// Get get job info +// See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer-job +func (a *PeerJobsAPI) Get(ctx context.Context, jobID string) (*api.JobResponse, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/jobs/"+jobID, nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.JobResponse](resp) + return &ret, err +} + +// Create create new job for a peer +// See more: https://docs.netbird.io/api/resources/peers#create-a-peer-job +func (a *PeerJobsAPI) Create(ctx context.Context, request api.PostApiPeersPeerIdJobsJSONRequestBody) (*api.JobResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/peers/"+a.peerID+"/jobs", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.JobResponse](resp) + return &ret, err +} diff --git a/shared/management/client/rest/peers_test.go b/shared/management/client/rest/peers_test.go index c464de7ed..5724b57f9 100644 --- a/shared/management/client/rest/peers_test.go +++ b/shared/management/client/rest/peers_test.go @@ -25,6 +25,21 @@ var ( DnsLabel: "test", Id: "Test", } + + testPeerTemporaryAccess = api.PeerTemporaryAccessResponse{ + Id: "Test", + Name: "test-peer", + } + + testIngressPortAllocation = api.IngressPortAllocation{ + Enabled: true, + Id: "alloc-1", + } + + testJobResponse = api.JobResponse{ + Id: "job-1", + Status: "pending", + } ) func TestPeers_List_200(t *testing.T) { @@ -177,6 +192,264 @@ func TestPeers_ListAccessiblePeers_Err(t *testing.T) { }) } +func TestPeers_CreateTemporaryAccess_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/temporary-access", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testPeerTemporaryAccess) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.CreateTemporaryAccess(context.Background(), "Test", api.PostApiPeersPeerIdTemporaryAccessJSONRequestBody{}) + require.NoError(t, err) + assert.Equal(t, testPeerTemporaryAccess, *ret) + }) +} + +func TestPeers_CreateTemporaryAccess_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/temporary-access", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.CreateTemporaryAccess(context.Background(), "Test", api.PostApiPeersPeerIdTemporaryAccessJSONRequestBody{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPeerIngressPorts_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.IngressPortAllocation{testIngressPortAllocation}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testIngressPortAllocation, ret[0]) + }) +} + +func TestPeerIngressPorts_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeerIngressPorts_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testIngressPortAllocation) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").Get(context.Background(), "alloc-1") + require.NoError(t, err) + assert.Equal(t, testIngressPortAllocation, *ret) + }) +} + +func TestPeerIngressPorts_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").Get(context.Background(), "alloc-1") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeerIngressPorts_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testIngressPortAllocation) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").Create(context.Background(), api.PostApiPeersPeerIdIngressPortsJSONRequestBody{}) + require.NoError(t, err) + assert.Equal(t, testIngressPortAllocation, *ret) + }) +} + +func TestPeerIngressPorts_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").Create(context.Background(), api.PostApiPeersPeerIdIngressPortsJSONRequestBody{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPeerIngressPorts_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + retBytes, _ := json.Marshal(testIngressPortAllocation) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").Update(context.Background(), "alloc-1", api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody{}) + require.NoError(t, err) + assert.Equal(t, testIngressPortAllocation, *ret) + }) +} + +func TestPeerIngressPorts_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.IngressPorts("Test").Update(context.Background(), "alloc-1", api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPeerIngressPorts_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Peers.IngressPorts("Test").Delete(context.Background(), "alloc-1") + require.NoError(t, err) + }) +} + +func TestPeerIngressPorts_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Peers.IngressPorts("Test").Delete(context.Background(), "alloc-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPeerJobs_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/jobs", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.JobResponse{testJobResponse}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Jobs("Test").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testJobResponse.Id, ret[0].Id) + assert.Equal(t, testJobResponse.Status, ret[0].Status) + }) +} + +func TestPeerJobs_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/jobs", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Jobs("Test").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeerJobs_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/jobs/job-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testJobResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Jobs("Test").Get(context.Background(), "job-1") + require.NoError(t, err) + assert.Equal(t, testJobResponse.Id, ret.Id) + assert.Equal(t, testJobResponse.Status, ret.Status) + }) +} + +func TestPeerJobs_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/jobs/job-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Jobs("Test").Get(context.Background(), "job-1") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeerJobs_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/jobs", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testJobResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Jobs("Test").Create(context.Background(), api.PostApiPeersPeerIdJobsJSONRequestBody{}) + require.NoError(t, err) + assert.Equal(t, testJobResponse.Id, ret.Id) + assert.Equal(t, testJobResponse.Status, ret.Status) + }) +} + +func TestPeerJobs_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/jobs", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Jobs("Test").Create(context.Background(), api.PostApiPeersPeerIdJobsJSONRequestBody{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + func TestPeers_Integration(t *testing.T) { withBlackBoxServer(t, func(c *rest.Client) { peers, err := c.Peers.List(context.Background()) diff --git a/shared/management/client/rest/scim.go b/shared/management/client/rest/scim.go new file mode 100644 index 000000000..f9a33fee7 --- /dev/null +++ b/shared/management/client/rest/scim.go @@ -0,0 +1,119 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/shared/management/http/api" +) + +// SCIMAPI APIs for SCIM IDP integrations +type SCIMAPI struct { + c *Client +} + +// List retrieves all SCIM IDP integrations +// See more: https://docs.netbird.io/api/resources/scim#list-all-scim-integrations +func (a *SCIMAPI) List(ctx context.Context) ([]api.ScimIntegration, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/scim-idp", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.ScimIntegration](resp) + return ret, err +} + +// Get retrieves a specific SCIM IDP integration by ID +// See more: https://docs.netbird.io/api/resources/scim#retrieve-a-scim-integration +func (a *SCIMAPI) Get(ctx context.Context, integrationID string) (*api.ScimIntegration, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/scim-idp/"+integrationID, nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.ScimIntegration](resp) + return &ret, err +} + +// Create creates a new SCIM IDP integration +// See more: https://docs.netbird.io/api/resources/scim#create-a-scim-integration +func (a *SCIMAPI) Create(ctx context.Context, request api.CreateScimIntegrationRequest) (*api.ScimIntegration, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/scim-idp", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.ScimIntegration](resp) + return &ret, err +} + +// Update updates an existing SCIM IDP integration +// See more: https://docs.netbird.io/api/resources/scim#update-a-scim-integration +func (a *SCIMAPI) Update(ctx context.Context, integrationID string, request api.UpdateScimIntegrationRequest) (*api.ScimIntegration, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/scim-idp/"+integrationID, bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.ScimIntegration](resp) + return &ret, err +} + +// Delete deletes a SCIM IDP integration +// See more: https://docs.netbird.io/api/resources/scim#delete-a-scim-integration +func (a *SCIMAPI) Delete(ctx context.Context, integrationID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/scim-idp/"+integrationID, nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + return nil +} + +// RegenerateToken regenerates the SCIM API token for an integration +// See more: https://docs.netbird.io/api/resources/scim#regenerate-scim-token +func (a *SCIMAPI) RegenerateToken(ctx context.Context, integrationID string) (*api.ScimTokenResponse, error) { + resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/scim-idp/"+integrationID+"/token", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.ScimTokenResponse](resp) + return &ret, err +} + +// GetLogs retrieves synchronization logs for an SCIM IDP integration +// See more: https://docs.netbird.io/api/resources/scim#get-scim-sync-logs +func (a *SCIMAPI) GetLogs(ctx context.Context, integrationID string) ([]api.IdpIntegrationSyncLog, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/scim-idp/"+integrationID+"/logs", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.IdpIntegrationSyncLog](resp) + return ret, err +} diff --git a/shared/management/client/rest/scim_test.go b/shared/management/client/rest/scim_test.go new file mode 100644 index 000000000..08581b482 --- /dev/null +++ b/shared/management/client/rest/scim_test.go @@ -0,0 +1,262 @@ +//go:build integration + +package rest_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" +) + +var ( + testScimIntegration = api.ScimIntegration{ + Id: 1, + AuthToken: "****", + Enabled: true, + GroupPrefixes: []string{"eng-"}, + UserGroupPrefixes: []string{"dev-"}, + Provider: "okta", + LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + } + + testScimToken = api.ScimTokenResponse{ + AuthToken: "new-token-123", + } + + testSyncLog = api.IdpIntegrationSyncLog{ + Id: 1, + Level: "info", + Message: "Sync completed", + Timestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + } +) + +func TestSCIM_List_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.ScimIntegration{testScimIntegration}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testScimIntegration, ret[0]) + }) +} + +func TestSCIM_List_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestSCIM_Get_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal(testScimIntegration) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.Get(context.Background(), "int-1") + require.NoError(t, err) + assert.Equal(t, testScimIntegration, *ret) + }) +} + +func TestSCIM_Get_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.Get(context.Background(), "int-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSCIM_Create_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.CreateScimIntegrationRequest + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "okta", req.Provider) + assert.Equal(t, "scim-", req.Prefix) + retBytes, _ := json.Marshal(testScimIntegration) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.Create(context.Background(), api.CreateScimIntegrationRequest{ + Provider: "okta", + Prefix: "scim-", + GroupPrefixes: &[]string{"eng-"}, + }) + require.NoError(t, err) + assert.Equal(t, testScimIntegration, *ret) + }) +} + +func TestSCIM_Create_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.Create(context.Background(), api.CreateScimIntegrationRequest{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSCIM_Update_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.UpdateScimIntegrationRequest + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, *req.Enabled) + retBytes, _ := json.Marshal(testScimIntegration) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.Update(context.Background(), "int-1", api.UpdateScimIntegrationRequest{ + Enabled: ptr(true), + }) + require.NoError(t, err) + assert.Equal(t, testScimIntegration, *ret) + }) +} + +func TestSCIM_Update_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.Update(context.Background(), "int-1", api.UpdateScimIntegrationRequest{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSCIM_Delete_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.SCIM.Delete(context.Background(), "int-1") + require.NoError(t, err) + }) +} + +func TestSCIM_Delete_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.SCIM.Delete(context.Background(), "int-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestSCIM_RegenerateToken_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1/token", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testScimToken) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.RegenerateToken(context.Background(), "int-1") + require.NoError(t, err) + assert.Equal(t, testScimToken, *ret) + }) +} + +func TestSCIM_RegenerateToken_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1/token", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.RegenerateToken(context.Background(), "int-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSCIM_GetLogs_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + retBytes, _ := json.Marshal([]api.IdpIntegrationSyncLog{testSyncLog}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.GetLogs(context.Background(), "int-1") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testSyncLog, ret[0]) + }) +} + +func TestSCIM_GetLogs_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/integrations/scim-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SCIM.GetLogs(context.Background(), "int-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + assert.Empty(t, ret) + }) +} diff --git a/shared/management/client/rest/users.go b/shared/management/client/rest/users.go index b0ea46d55..98d84895b 100644 --- a/shared/management/client/rest/users.go +++ b/shared/management/client/rest/users.go @@ -105,3 +105,145 @@ func (a *UsersAPI) Current(ctx context.Context) (*api.User, error) { ret, err := parseResponse[api.User](resp) return &ret, err } + +// ListInvites list all user invites +// See more: https://docs.netbird.io/api/resources/users#list-all-user-invites +func (a *UsersAPI) ListInvites(ctx context.Context) ([]api.UserInvite, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/invites", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[[]api.UserInvite](resp) + return ret, err +} + +// CreateInvite create a user invite +// See more: https://docs.netbird.io/api/resources/users#create-a-user-invite +func (a *UsersAPI) CreateInvite(ctx context.Context, request api.PostApiUsersInvitesJSONRequestBody) (*api.UserInvite, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/invites", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.UserInvite](resp) + return &ret, err +} + +// DeleteInvite delete a user invite +// See more: https://docs.netbird.io/api/resources/users#delete-a-user-invite +func (a *UsersAPI) DeleteInvite(ctx context.Context, inviteID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/invites/"+inviteID, nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} + +// RegenerateInvite regenerate a user invite token +// See more: https://docs.netbird.io/api/resources/users#regenerate-a-user-invite +func (a *UsersAPI) RegenerateInvite(ctx context.Context, inviteID string, request api.PostApiUsersInvitesInviteIdRegenerateJSONRequestBody) (*api.UserInviteRegenerateResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/invites/"+inviteID+"/regenerate", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.UserInviteRegenerateResponse](resp) + return &ret, err +} + +// GetInviteByToken get a user invite by token +// See more: https://docs.netbird.io/api/resources/users#get-a-user-invite-by-token +func (a *UsersAPI) GetInviteByToken(ctx context.Context, token string) (*api.UserInviteInfo, error) { + resp, err := a.c.NewRequest(ctx, "GET", "/api/users/invites/"+token, nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.UserInviteInfo](resp) + return &ret, err +} + +// AcceptInvite accept a user invite +// See more: https://docs.netbird.io/api/resources/users#accept-a-user-invite +func (a *UsersAPI) AcceptInvite(ctx context.Context, token string, request api.PostApiUsersInvitesTokenAcceptJSONRequestBody) (*api.UserInviteAcceptResponse, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/invites/"+token+"/accept", bytes.NewReader(requestBytes), nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.UserInviteAcceptResponse](resp) + return &ret, err +} + +// Approve approve a pending user +// See more: https://docs.netbird.io/api/resources/users#approve-a-user +func (a *UsersAPI) Approve(ctx context.Context, userID string) (*api.User, error) { + resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/approve", nil, nil) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + ret, err := parseResponse[api.User](resp) + return &ret, err +} + +// ChangePassword change a user's password +// See more: https://docs.netbird.io/api/resources/users#change-user-password +func (a *UsersAPI) ChangePassword(ctx context.Context, userID string, request api.PutApiUsersUserIdPasswordJSONRequestBody) error { + requestBytes, err := json.Marshal(request) + if err != nil { + return err + } + resp, err := a.c.NewRequest(ctx, "PUT", "/api/users/"+userID+"/password", bytes.NewReader(requestBytes), nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} + +// Reject reject a pending user +// See more: https://docs.netbird.io/api/resources/users#reject-a-user +func (a *UsersAPI) Reject(ctx context.Context, userID string) error { + resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID+"/reject", nil, nil) + if err != nil { + return err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return nil +} diff --git a/shared/management/client/rest/users_test.go b/shared/management/client/rest/users_test.go index 68815d4f9..66690833a 100644 --- a/shared/management/client/rest/users_test.go +++ b/shared/management/client/rest/users_test.go @@ -32,6 +32,23 @@ var ( Role: "user", Status: api.UserStatusActive, } + + testUserInvite = api.UserInvite{ + AutoGroups: []string{"group1"}, + Id: "invite-1", + } + + testUserInviteInfo = api.UserInviteInfo{ + Email: "invite@test.com", + } + + testUserInviteAcceptResponse = api.UserInviteAcceptResponse{ + Success: true, + } + + testUserInviteRegenerateResponse = api.UserInviteRegenerateResponse{ + InviteToken: "new-token", + } ) func TestUsers_List_200(t *testing.T) { @@ -220,6 +237,269 @@ func TestUsers_Current_Err(t *testing.T) { }) } +func TestUsers_ListInvites_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.UserInvite{testUserInvite}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.ListInvites(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testUserInvite, ret[0]) + }) +} + +func TestUsers_ListInvites_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.ListInvites(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestUsers_CreateInvite_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiUsersInvitesJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "invite@test.com", req.Email) + retBytes, _ := json.Marshal(testUserInvite) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.CreateInvite(context.Background(), api.PostApiUsersInvitesJSONRequestBody{ + Email: "invite@test.com", + }) + require.NoError(t, err) + assert.Equal(t, testUserInvite, *ret) + }) +} + +func TestUsers_CreateInvite_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.CreateInvite(context.Background(), api.PostApiUsersInvitesJSONRequestBody{ + Email: "invite@test.com", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_DeleteInvite_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/invite-1", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Users.DeleteInvite(context.Background(), "invite-1") + require.NoError(t, err) + }) +} + +func TestUsers_DeleteInvite_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/invite-1", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.DeleteInvite(context.Background(), "invite-1") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestUsers_RegenerateInvite_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/invite-1/regenerate", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testUserInviteRegenerateResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.RegenerateInvite(context.Background(), "invite-1", api.PostApiUsersInvitesInviteIdRegenerateJSONRequestBody{}) + require.NoError(t, err) + assert.Equal(t, testUserInviteRegenerateResponse, *ret) + }) +} + +func TestUsers_RegenerateInvite_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/invite-1/regenerate", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.RegenerateInvite(context.Background(), "invite-1", api.PostApiUsersInvitesInviteIdRegenerateJSONRequestBody{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_GetInviteByToken_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/some-token", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testUserInviteInfo) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.GetInviteByToken(context.Background(), "some-token") + require.NoError(t, err) + assert.Equal(t, testUserInviteInfo, *ret) + }) +} + +func TestUsers_GetInviteByToken_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/some-token", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.GetInviteByToken(context.Background(), "some-token") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestUsers_AcceptInvite_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/some-token/accept", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testUserInviteAcceptResponse) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.AcceptInvite(context.Background(), "some-token", api.PostApiUsersInvitesTokenAcceptJSONRequestBody{}) + require.NoError(t, err) + assert.Equal(t, testUserInviteAcceptResponse, *ret) + }) +} + +func TestUsers_AcceptInvite_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/invites/some-token/accept", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.AcceptInvite(context.Background(), "some-token", api.PostApiUsersInvitesTokenAcceptJSONRequestBody{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_Approve_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/approve", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + retBytes, _ := json.Marshal(testUser) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Approve(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testUser, *ret) + }) +} + +func TestUsers_Approve_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/approve", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Approve(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_ChangePassword_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/password", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiUsersUserIdPasswordJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + w.WriteHeader(200) + }) + err := c.Users.ChangePassword(context.Background(), "Test", api.PutApiUsersUserIdPasswordJSONRequestBody{}) + require.NoError(t, err) + }) +} + +func TestUsers_ChangePassword_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/password", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.ChangePassword(context.Background(), "Test", api.PutApiUsersUserIdPasswordJSONRequestBody{}) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + }) +} + +func TestUsers_Reject_200(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/reject", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Users.Reject(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestUsers_Reject_Err(t *testing.T) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/reject", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.Reject(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + }) +} + func TestUsers_Integration(t *testing.T) { withBlackBoxServer(t, func(c *rest.Client) { // rest client PAT is owner's diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index b9a8eae3a..5a504c471 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -42,6 +42,52 @@ tags: description: Interact with and view information about remote jobs. x-experimental: true + - name: Usage + description: Retrieve current usage statistics for the account. + x-cloud-only: true + - name: Subscription + description: Manage and view information about account subscriptions. + x-cloud-only: true + - name: Plans + description: Retrieve available plans and products. + x-cloud-only: true + - name: Checkout + description: Manage checkout sessions for plan subscriptions. + x-cloud-only: true + - name: AWS Marketplace + description: Manage AWS Marketplace subscriptions. + x-cloud-only: true + - name: Portal + description: Access customer portal for subscription management. + x-cloud-only: true + - name: Invoice + description: Manage and retrieve account invoices. + x-cloud-only: true + - name: MSP + description: MSP portal for Tenant management. + x-cloud-only: true + - name: IDP + description: Manage identity provider integrations for user and group sync. + x-cloud-only: true + - name: EDR Intune Integrations + description: Manage Microsoft Intune EDR integrations. + x-cloud-only: true + - name: EDR SentinelOne Integrations + description: Manage SentinelOne EDR integrations. + x-cloud-only: true + - name: EDR Falcon Integrations + description: Manage CrowdStrike Falcon EDR integrations. + x-cloud-only: true + - name: EDR Huntress Integrations + description: Manage Huntress EDR integrations. + x-cloud-only: true + - name: EDR Peers + description: Manage EDR compliance bypass for peers. + x-cloud-only: true + - name: Event Streaming Integrations + description: Manage event streaming integrations. + x-cloud-only: true + components: schemas: PasswordChangeRequest: @@ -61,8 +107,8 @@ components: WorkloadType: type: string description: | - Identifies the type of workload the job will execute. - Currently only `"bundle"` is supported. + Identifies the type of workload the job will execute. + Currently only `"bundle"` is supported. enum: - bundle example: "bundle" @@ -110,8 +156,8 @@ components: parameters: $ref: '#/components/schemas/BundleParameters' required: - - type - - parameters + - type + - parameters BundleWorkloadResponse: type: object properties: @@ -162,7 +208,7 @@ components: type: string status: type: string - enum: [pending, succeeded, failed] + enum: [ pending, succeeded, failed ] failed_reason: type: string nullable: true @@ -371,7 +417,7 @@ components: status: description: User's status type: string - enum: [ "active","invited","blocked" ] + enum: [ "active", "invited", "blocked" ] example: active last_login: description: Last time this user performed a login to the dashboard @@ -439,7 +485,7 @@ components: propertyNames: type: string description: The module name - example: {"networks": { "read": true, "create": false, "update": false, "delete": false}, "peers": { "read": false, "create": false, "update": false, "delete": false} } + example: { "networks": { "read": true, "create": false, "update": false, "delete": false }, "peers": { "read": false, "create": false, "update": false, "delete": false } } required: - modules - is_restricted @@ -1234,7 +1280,7 @@ components: issued: description: How the group was issued (api, integration, jwt) type: string - enum: ["api", "integration", "jwt"] + enum: [ "api", "integration", "jwt" ] example: api required: - id @@ -1295,7 +1341,7 @@ components: action: description: Policy rule accept or drops packets type: string - enum: ["accept","drop"] + enum: [ "accept", "drop" ] example: "accept" bidirectional: description: Define if the rule is applicable in both directions, sources, and destinations. @@ -1304,7 +1350,7 @@ components: protocol: description: Policy rule type of the traffic type: string - enum: ["all", "tcp", "udp", "icmp", "netbird-ssh"] + enum: [ "all", "tcp", "udp", "icmp", "netbird-ssh" ] example: "tcp" ports: description: Policy rule affected ports @@ -1615,7 +1661,7 @@ components: type: array items: type: string - example: ["192.168.1.0/24", "10.0.0.0/8", "2001:db8:1234:1a00::/56"] + example: [ "192.168.1.0/24", "10.0.0.0/8", "2001:db8:1234:1a00::/56" ] action: description: Action to take upon policy match type: string @@ -1786,11 +1832,11 @@ components: - description - network_id - enabled - # Only one property has to be set - #- peer - #- peer_groups - # Only one property has to be set - #- network + # Only one property has to be set + #- peer + #- peer_groups + # Only one property has to be set + #- network #- domains - metric - masquerade @@ -1829,7 +1875,7 @@ components: allOf: - $ref: '#/components/schemas/NetworkResourceType' - type: string - enum: ["peer"] + enum: [ "peer" ] example: peer NetworkRequest: type: object @@ -2198,52 +2244,7 @@ components: activity_code: description: The string code of the activity that occurred during the event type: string - enum: [ - "peer.user.add", "peer.setupkey.add", "user.join", "user.invite", "account.create", "account.delete", - "user.peer.delete", "rule.add", "rule.update", "rule.delete", - "policy.add", "policy.update", "policy.delete", - "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse", "setupkey.delete", - "group.add", "group.update", "group.delete", - "peer.group.add", "peer.group.delete", - "user.group.add", "user.group.delete", "user.role.update", - "setupkey.group.add", "setupkey.group.delete", - "dns.setting.disabled.management.group.add", "dns.setting.disabled.management.group.delete", - "route.add", "route.delete", "route.update", - "peer.ssh.enable", "peer.ssh.disable", "peer.rename", - "peer.login.expiration.enable", "peer.login.expiration.disable", - "nameserver.group.add", "nameserver.group.delete", "nameserver.group.update", - "account.setting.peer.login.expiration.update", "account.setting.peer.login.expiration.enable", "account.setting.peer.login.expiration.disable", - "personal.access.token.create", "personal.access.token.delete", - "service.user.create", "service.user.delete", - "user.block", "user.unblock", "user.delete", - "user.peer.login", "peer.login.expire", - "dashboard.login", - "integration.create", "integration.update", "integration.delete", - "account.setting.peer.approval.enable", "account.setting.peer.approval.disable", - "peer.approve", "peer.approval.revoke", - "transferred.owner.role", - "posture.check.create", "posture.check.update", "posture.check.delete", - "peer.inactivity.expiration.enable", "peer.inactivity.expiration.disable", - "account.peer.inactivity.expiration.enable", "account.peer.inactivity.expiration.disable", "account.peer.inactivity.expiration.update", - "account.setting.group.propagation.enable", "account.setting.group.propagation.disable", - "account.setting.routing.peer.dns.resolution.enable", "account.setting.routing.peer.dns.resolution.disable", - "network.create", "network.update", "network.delete", - "network.resource.create", "network.resource.update", "network.resource.delete", - "network.router.create", "network.router.update", "network.router.delete", - "resource.group.add", "resource.group.delete", - "account.dns.domain.update", - "account.setting.lazy.connection.enable", "account.setting.lazy.connection.disable", - "account.network.range.update", - "peer.ip.update", - "user.approve", "user.reject", "user.create", - "account.settings.auto.version.update", - "identityprovider.create", "identityprovider.update", "identityprovider.delete", - "dns.zone.create", "dns.zone.update", "dns.zone.delete", - "dns.zone.record.create", "dns.zone.record.update", "dns.zone.record.delete", - "peer.job.create", - "user.password.change", - "user.invite.link.create", "user.invite.link.accept", "user.invite.link.regenerate", "user.invite.link.delete" - ] + enum: [ "peer.user.add", "peer.setupkey.add", "user.join", "user.invite", "account.create", "account.delete", "user.peer.delete", "rule.add", "rule.update", "rule.delete", "policy.add", "policy.update", "policy.delete", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse", "setupkey.delete", "group.add", "group.update", "group.delete", "peer.group.add", "peer.group.delete", "user.group.add", "user.group.delete", "user.role.update", "setupkey.group.add", "setupkey.group.delete", "dns.setting.disabled.management.group.add", "dns.setting.disabled.management.group.delete", "route.add", "route.delete", "route.update", "peer.ssh.enable", "peer.ssh.disable", "peer.rename", "peer.login.expiration.enable", "peer.login.expiration.disable", "nameserver.group.add", "nameserver.group.delete", "nameserver.group.update", "account.setting.peer.login.expiration.update", "account.setting.peer.login.expiration.enable", "account.setting.peer.login.expiration.disable", "personal.access.token.create", "personal.access.token.delete", "service.user.create", "service.user.delete", "user.block", "user.unblock", "user.delete", "user.peer.login", "peer.login.expire", "dashboard.login", "integration.create", "integration.update", "integration.delete", "account.setting.peer.approval.enable", "account.setting.peer.approval.disable", "peer.approve", "peer.approval.revoke", "transferred.owner.role", "posture.check.create", "posture.check.update", "posture.check.delete", "peer.inactivity.expiration.enable", "peer.inactivity.expiration.disable", "account.peer.inactivity.expiration.enable", "account.peer.inactivity.expiration.disable", "account.peer.inactivity.expiration.update", "account.setting.group.propagation.enable", "account.setting.group.propagation.disable", "account.setting.routing.peer.dns.resolution.enable", "account.setting.routing.peer.dns.resolution.disable", "network.create", "network.update", "network.delete", "network.resource.create", "network.resource.update", "network.resource.delete", "network.router.create", "network.router.update", "network.router.delete", "resource.group.add", "resource.group.delete", "account.dns.domain.update", "account.setting.lazy.connection.enable", "account.setting.lazy.connection.disable", "account.network.range.update", "peer.ip.update", "user.approve", "user.reject", "user.create", "account.settings.auto.version.update", "identityprovider.create", "identityprovider.update", "identityprovider.delete", "dns.zone.create", "dns.zone.update", "dns.zone.delete", "dns.zone.record.create", "dns.zone.record.update", "dns.zone.record.delete", "peer.job.create", "user.password.change", "user.invite.link.create", "user.invite.link.accept", "user.invite.link.regenerate", "user.invite.link.delete" ] example: route.add initiator_id: description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event. @@ -2266,7 +2267,7 @@ components: type: object additionalProperties: type: string - example: { "name": "my route", "network_range": "10.64.0.0/24", "peer_id": "chacbco6lnnbn6cg5s91"} + example: { "name": "my route", "network_range": "10.64.0.0/24", "peer_id": "chacbco6lnnbn6cg5s91" } required: - id - timestamp @@ -2558,9 +2559,9 @@ components: description: "Email of the user who initiated the event (if any)." example: "alice@netbird.io" name: - type: string - description: "Name of the user who initiated the event (if any)." - example: "Alice Smith" + type: string + description: "Name of the user who initiated the event (if any)." + example: "Alice Smith" required: - id - email @@ -2836,6 +2837,980 @@ components: required: - management_current_version - management_update_available + UsageStats: + type: object + properties: + active_users: + type: integer + format: int64 + description: Number of active users. + example: 15 + total_users: + type: integer + format: int64 + description: Total number of users. + example: 20 + active_peers: + type: integer + format: int64 + description: Number of active peers. + example: 10 + total_peers: + type: integer + format: int64 + description: Total number of peers. + example: 25 + required: + - active_users + - total_users + - active_peers + - total_peers + Product: + type: object + properties: + name: + type: string + description: Name of the product. + example: "Basic Plan" + description: + type: string + description: Detailed description of the product. + example: "This is the basic plan with limited features." + features: + type: array + description: List of features provided by the product. + items: + type: string + example: [ "5 free users", "Basic support" ] + prices: + type: array + description: List of prices for the product in different currencies + items: + $ref: "#/components/schemas/Price" + free: + type: boolean + description: Indicates whether the product is free or not. + example: false + required: + - name + - description + - features + - prices + - free + Price: + type: object + properties: + price_id: + type: string + description: Unique identifier for the price. + example: "price_H2KmRb4u1tP0sR7s" + currency: + type: string + description: Currency code for this price. + example: "USD" + price: + type: integer + description: Price amount in minor units (e.g., cents). + example: 1000 + unit: + type: string + description: Unit of measurement for this price (e.g., per user). + example: "user" + required: + - price_id + - currency + - price + - unit + Subscription: + type: object + properties: + active: + type: boolean + description: Indicates whether the subscription is active or not. + example: true + plan_tier: + type: string + description: The tier of the plan for the subscription. + example: "basic" + price_id: + type: string + description: Unique identifier for the price of the subscription. + example: "price_1HhxOpBzq4JbCqRmJxkpzL2V" + remaining_trial: + type: integer + description: The remaining time for the trial period, in seconds. + example: 3600 + features: + type: array + description: List of features included in the subscription. + items: + type: string + example: [ "free", "idp-sync", "audit-logs" ] + currency: + type: string + description: Currency code of the subscription. + example: "USD" + price: + type: integer + description: Price amount in minor units (e.g., cents). + example: 1000 + provider: + type: string + description: The provider of the subscription. + example: [ "stripe", "aws" ] + updated_at: + type: string + format: date-time + description: The date and time when the subscription was last updated. + example: "2021-08-01T12:00:00Z" + required: + - active + - plan_tier + - price_id + - updated_at + - currency + - price + - provider + PortalResponse: + type: object + properties: + session_id: + type: string + description: The unique identifier for the customer portal session. + example: "cps_test_123456789" + url: + type: string + description: URL to redirect the user to the customer portal. + example: "https://billing.stripe.com/session/a1b2c3d4e5f6g7h8i9j0k" + required: + - session_id + - url + CheckoutResponse: + type: object + properties: + session_id: + type: string + description: The unique identifier for the checkout session. + example: "cs_test_a1b2c3d4e5f6g7h8i9j0" + url: + type: string + description: URL to redirect the user to the checkout session. + example: "https://checkout.stripe.com/pay/cs_test_a1b2c3d4e5f6g7h8i9j0" + required: + - session_id + - url + StripeWebhookEvent: + type: object + properties: + type: + type: string + description: The type of event received from Stripe. + example: "customer.subscription.updated" + data: + type: object + description: The data associated with the event from Stripe. + example: + object: + id: "sub_123456789" + object: "subscription" + status: "active" + items: + object: "list" + data: + - id: "si_123456789" + object: "subscription_item" + price: + id: "price_1HhxOpBzq4JbCqRmJxkpzL2V" + object: "price" + unit_amount: 2000 + currency: "usd" + billing_cycle_anchor: 1609459200 + InvoiceResponse: + type: object + properties: + id: + type: string + description: The Stripe invoice id + example: "in_1MtHbELkdIwHu7ixl4OzzPMv" + type: + type: string + description: The invoice type + enum: + - account + - tenants + period_start: + type: string + format: date-time + description: The start date of the invoice period. + example: "2021-08-01T12:00:00Z" + period_end: + type: string + format: date-time + description: The end date of the invoice period. + example: "2021-08-31T12:00:00Z" + required: + - id + - type + - period_start + - period_end + InvoicePDFResponse: + type: object + properties: + url: + type: string + description: URL to redirect the user to invoice. + example: "https://invoice.stripe.com/i/acct_1M2DaBKina4I2KUb/test_YWNjdF8xTTJEdVBLaW5hM0kyS1ViLF1SeFpQdEJZd3lUOGNEajNqeWdrdXY2RFM4aHcyCnpsLDEzMjg3GTgyNQ02000JoIHc1X?s=db" + required: + - url + CreateTenantRequest: + type: object + properties: + name: + type: string + description: The name for the MSP tenant + example: "My new tenant" + domain: + type: string + description: The name for the MSP tenant + example: "tenant.com" + groups: + description: MSP users Groups that can access the Tenant and Roles to assume + type: array + items: + $ref: "#/components/schemas/TenantGroupResponse" + required: + - name + - domain + - groups + UpdateTenantRequest: + type: object + properties: + name: + type: string + description: The name for the MSP tenant + example: "My new tenant" + groups: + description: MSP users Groups that can access the Tenant and Roles to assume + type: array + items: + $ref: "#/components/schemas/TenantGroupResponse" + required: + - name + - groups + GetTenantsResponse: + type: array + items: + $ref: "#/components/schemas/TenantResponse" + DNSChallengeResponse: + type: object + properties: + dns_challenge: + type: string + description: The DNS challenge to set in a TXT record + example: YXNkYSBkYXNhc2Rhc2RhIGFzZGFzZDJhc2QyNDUxNQ + required: + - dns_challenge + TenantGroupResponse: + type: object + properties: + id: + type: string + description: The Group ID + example: ch8i4ug6lnn4g9hqv7m0 + role: + type: string + description: The Role name + example: "admin" + required: + - id + - role + TenantResponse: + type: object + properties: + id: + type: string + description: The updated MSP tenant account ID + example: ch8i4ug6lnn4g9hqv7m0 + name: + type: string + description: The name for the MSP tenant + example: "My new tenant" + domain: + type: string + description: The tenant account domain + example: "tenant.com" + groups: + description: MSP users Groups that can access the Tenant and Roles to assume + type: array + items: + $ref: "#/components/schemas/TenantGroupResponse" + activated_at: + type: string + format: date-time + description: The date and time when the tenant was activated. + example: "2021-08-01T12:00:00Z" + dns_challenge: + type: string + description: The DNS challenge to set in a TXT record + example: YXNkYSBkYXNhc2Rhc2RhIGFzZGFzZDJhc2QyNDUxNQ + created_at: + type: string + format: date-time + description: The date and time when the tenant was created. + example: "2021-08-01T12:00:00Z" + updated_at: + type: string + format: date-time + description: The date and time when the tenant was last updated. + example: "2021-08-01T12:00:00Z" + invited_at: + type: string + format: date-time + description: The date and time when the existing tenant was invited. + example: "2021-08-01T12:00:00Z" + status: + type: string + description: The status of the tenant + enum: + - existing + - invited + - pending + - active + example: "active" + required: + - id + - name + - domain + - groups + - created_at + - updated_at + - status + - dns_challenge + CreateIntegrationRequest: + type: object + description: "Request payload for creating a new event streaming integration. Also used as the structure for the PUT request body, but not all fields are applicable for updates (see PUT operation description)." + required: + - platform + - config + - enabled + properties: + platform: + type: string + description: The event streaming platform to integrate with (e.g., "datadog", "s3", "firehose"). This field is used for creation. For updates (PUT), this field, if sent, is ignored by the backend. + enum: [ "datadog", "s3", "firehose", "generic_http" ] + example: "s3" + config: + type: object + additionalProperties: + type: string + description: Platform-specific configuration as key-value pairs. For creation, all necessary credentials and settings must be provided. For updates, provide the fields to change or the entire new configuration. + example: { "bucket_name": "my-event-logs", "region": "us-east-1", "access_key_id": "AKIA...", "secret_access_key": "YOUR_SECRET_KEY" } + enabled: + type: boolean + description: "Specifies whether the integration is enabled. During creation (POST), this value is sent by the client, but the provided backend manager function `CreateIntegration` does not appear to use it directly, so its effect on creation should be verified. During updates (PUT), this field is used to enable or disable the integration." + example: true + IntegrationResponse: + type: object + description: Represents an event streaming integration. + properties: + id: + type: integer + format: int64 + description: The unique numeric identifier for the integration. + example: 123 + minimum: 0 + account_id: + type: string + description: The identifier of the account this integration belongs to. + example: "acc_abcdef123456" + enabled: + type: boolean + description: Whether the integration is currently active. + example: true + platform: + type: string + description: The event streaming platform. + enum: [ "datadog", "s3", "firehose", "generic_http" ] + example: "datadog" + created_at: + type: string + format: date-time + description: Timestamp of when the integration was created. + example: "2023-05-15T10:30:00Z" + updated_at: + type: string + format: date-time + description: Timestamp of when the integration was last updated. + example: "2023-05-16T11:45:00Z" + config: + type: object + additionalProperties: + type: string + description: Configuration for the integration. Sensitive keys (like API keys, secret keys) are masked with '****' in responses, as indicated by the GetIntegration handler logic. + example: { "api_key": "****", "site": "datadoghq.com", "region": "us-east-1" } + EDRIntuneRequest: + type: object + description: "Request payload for creating or updating a EDR Intune integration." + required: + - client_id + - tenant_id + - secret + - groups + - last_synced_interval + properties: + client_id: + type: string + description: The Azure application client id + tenant_id: + type: string + description: The Azure tenant id + secret: + type: string + description: The Azure application client secret + groups: + type: array + description: The Groups this integrations applies to + items: + type: string + last_synced_interval: + type: integer + description: The devices last sync requirement interval in hours. Minimum value is 24 hours. + minimum: 24 + enabled: + type: boolean + description: Indicates whether the integration is enabled + default: true + EDRIntuneResponse: + type: object + description: Represents a Intune EDR integration configuration + required: + - id + - account_id + - created_by + - last_synced_at + - created_at + - updated_at + - client_id + - tenant_id + - groups + - last_synced_interval + - enabled + properties: + id: + type: integer + format: int64 + description: The unique numeric identifier for the integration. + example: 123 + minimum: 0 + account_id: + type: string + description: The identifier of the account this integration belongs to. + example: "acc_abcdef123456" + last_synced_at: + type: string + format: date-time + description: Timestamp of when the integration was last synced. + example: "2023-05-15T10:30:00Z" + created_by: + type: string + description: The user id that created the integration + created_at: + type: string + format: date-time + description: Timestamp of when the integration was created. + example: "2023-05-15T10:30:00Z" + updated_at: + type: string + format: date-time + description: Timestamp of when the integration was last updated. + example: "2023-05-16T11:45:00Z" + client_id: + type: string + description: The Azure application client id + example: "acc_abcdef123456" + tenant_id: + type: string + description: The Azure tenant id + example: "acc_abcdef123456" + groups: + type: array + description: List of groups + items: + $ref: '#/components/schemas/Group' + last_synced_interval: + type: integer + description: The devices last sync requirement interval in hours. + enabled: + type: boolean + description: Indicates whether the integration is enabled + EDRSentinelOneRequest: + type: object + description: Request payload for creating or updating a EDR SentinelOne integration + properties: + api_token: + type: string + description: SentinelOne API token + api_url: + type: string + description: The Base URL of SentinelOne API + groups: + type: array + description: The Groups this integrations applies to + items: + type: string + last_synced_interval: + type: integer + description: The devices last sync requirement interval in hours. Minimum value is 24 hours. + minimum: 24 + enabled: + type: boolean + description: Indicates whether the integration is enabled + default: true + match_attributes: + $ref: '#/components/schemas/SentinelOneMatchAttributes' + required: + - api_token + - api_url + - groups + - last_synced_interval + - match_attributes + EDRSentinelOneResponse: + type: object + description: Represents a SentinelOne EDR integration configuration + required: + - id + - account_id + - created_by + - last_synced_at + - created_at + - updated_at + - api_url + - groups + - last_synced_interval + - match_attributes + - enabled + properties: + id: + type: integer + format: int64 + description: The unique numeric identifier for the integration. + example: 123 + account_id: + type: string + description: The identifier of the account this integration belongs to. + example: "ch8i4ug6lnn4g9hqv7l0" + last_synced_at: + type: string + format: date-time + description: Timestamp of when the integration was last synced. + example: "2023-05-15T10:30:00Z" + created_by: + type: string + description: The user id that created the integration + created_at: + type: string + format: date-time + description: Timestamp of when the integration was created. + example: "2023-05-15T10:30:00Z" + updated_at: + type: string + format: date-time + description: Timestamp of when the integration was last updated. + example: "2023-05-16T11:45:00Z" + api_url: + type: string + description: The Base URL of SentinelOne API + groups: + type: array + description: List of groups + items: + $ref: '#/components/schemas/Group' + last_synced_interval: + type: integer + description: The devices last sync requirement interval in hours. + match_attributes: + $ref: '#/components/schemas/SentinelOneMatchAttributes' + enabled: + type: boolean + description: Indicates whether the integration is enabled + SentinelOneMatchAttributes: + type: object + description: Attribute conditions to match when approving agents + additionalProperties: false + properties: + active_threats: + description: The maximum allowed number of active threats on the agent + type: integer + example: 0 + encrypted_applications: + description: Whether disk encryption is enabled on the agent + type: boolean + firewall_enabled: + description: Whether the agent firewall is enabled + type: boolean + infected: + description: Whether the agent is currently flagged as infected + type: boolean + is_active: + description: Whether the agent has been recently active and reporting + type: boolean + is_up_to_date: + description: Whether the agent is running the latest available version + type: boolean + network_status: + description: The current network connectivity status of the device + type: string + enum: [ "connected", "disconnected", "quarantined" ] + operational_state: + description: The current operational state of the agent + type: string + + EDRFalconRequest: + type: object + description: Request payload for creating or updating a EDR Falcon integration + properties: + client_id: + type: string + description: CrowdStrike API client ID + secret: + type: string + description: CrowdStrike API client secret + cloud_id: + type: string + description: CrowdStrike cloud identifier (e.g., "us-1", "us-2", "eu-1") + groups: + type: array + description: The Groups this integration applies to + items: + type: string + zta_score_threshold: + type: integer + description: The minimum Zero Trust Assessment score required for agent approval (0-100) + minimum: 0 + maximum: 100 + example: 75 + enabled: + type: boolean + description: Indicates whether the integration is enabled + default: true + required: + - client_id + - secret + - cloud_id + - groups + - zta_score_threshold + EDRFalconResponse: + type: object + description: Represents a Falcon EDR integration + required: + - id + - account_id + - last_synced_at + - created_by + - created_at + - updated_at + - cloud_id + - groups + - zta_score_threshold + - enabled + properties: + id: + type: integer + format: int64 + description: The unique numeric identifier for the integration. + example: 123 + account_id: + type: string + description: The identifier of the account this integration belongs to. + example: "ch8i4ug6lnn4g9hqv7l0" + last_synced_at: + type: string + format: date-time + description: Timestamp of when the integration was last synced. + example: "2023-05-15T10:30:00Z" + created_by: + type: string + description: The user id that created the integration + created_at: + type: string + format: date-time + description: Timestamp of when the integration was created. + example: "2023-05-15T10:30:00Z" + updated_at: + type: string + format: date-time + description: Timestamp of when the integration was last updated. + example: "2023-05-16T11:45:00Z" + cloud_id: + type: string + description: CrowdStrike cloud identifier + groups: + type: array + description: List of groups + items: + $ref: '#/components/schemas/Group' + zta_score_threshold: + type: integer + description: The minimum Zero Trust Assessment score required for agent approval (0-100) + enabled: + type: boolean + description: Indicates whether the integration is enabled + + EDRHuntressRequest: + type: object + description: Request payload for creating or updating a EDR Huntress integration + properties: + api_key: + type: string + description: Huntress API key + api_secret: + type: string + description: Huntress API secret + groups: + type: array + description: The Groups this integrations applies to + items: + type: string + last_synced_interval: + type: integer + description: The devices last sync requirement interval in hours. Minimum value is 24 hours + minimum: 24 + enabled: + type: boolean + description: Indicates whether the integration is enabled + default: true + match_attributes: + $ref: '#/components/schemas/HuntressMatchAttributes' + required: + - api_key + - api_secret + - groups + - last_synced_interval + - match_attributes + EDRHuntressResponse: + type: object + description: Represents a Huntress EDR integration configuration + required: + - id + - account_id + - created_by + - last_synced_at + - created_at + - updated_at + - groups + - last_synced_interval + - match_attributes + - enabled + properties: + id: + type: integer + format: int64 + description: The unique numeric identifier for the integration. + example: 123 + account_id: + type: string + description: The identifier of the account this integration belongs to. + example: "ch8i4ug6lnn4g9hqv7l0" + last_synced_at: + type: string + format: date-time + description: Timestamp of when the integration was last synced. + example: "2023-05-15T10:30:00Z" + created_by: + type: string + description: The user id that created the integration + created_at: + type: string + format: date-time + description: Timestamp of when the integration was created. + example: "2023-05-15T10:30:00Z" + updated_at: + type: string + format: date-time + description: Timestamp of when the integration was last updated. + example: "2023-05-16T11:45:00Z" + groups: + type: array + description: List of groups + items: + $ref: '#/components/schemas/Group' + last_synced_interval: + type: integer + description: The devices last sync requirement interval in hours. + enabled: + type: boolean + description: Indicates whether the integration is enabled + default: true + match_attributes: + $ref: '#/components/schemas/HuntressMatchAttributes' + + HuntressMatchAttributes: + type: object + description: Attribute conditions to match when approving agents + additionalProperties: false + properties: + defender_policy_status: + type: string + description: Policy status of Defender AV for Managed Antivirus. + example: "Compliant" + defender_status: + type: string + description: Status of Defender AV Managed Antivirus. + example: "Healthy" + defender_substatus: + type: string + description: Sub-status of Defender AV Managed Antivirus. + example: "Up to date" + firewall_status: + type: string + description: Status of agent firewall. Can be one of Disabled, Enabled, Pending Isolation, Isolated, Pending Release. + example: "Enabled" + + CreateScimIntegrationRequest: + type: object + description: Request payload for creating an SCIM IDP integration + required: + - prefix + - provider + properties: + prefix: + type: string + description: The connection prefix used for the SCIM provider + provider: + type: string + description: Name of the SCIM identity provider + group_prefixes: + type: array + description: List of start_with string patterns for groups to sync + items: + type: string + example: [ "Engineering", "Sales" ] + user_group_prefixes: + type: array + description: List of start_with string patterns for groups which users to sync + items: + type: string + example: [ "Users" ] + UpdateScimIntegrationRequest: + type: object + description: Request payload for updating an SCIM IDP integration + properties: + enabled: + type: boolean + description: Indicates whether the integration is enabled + example: true + group_prefixes: + type: array + description: List of start_with string patterns for groups to sync + items: + type: string + example: [ "Engineering", "Sales" ] + user_group_prefixes: + type: array + description: List of start_with string patterns for groups which users to sync + items: + type: string + example: [ "Users" ] + ScimIntegration: + type: object + description: Represents a SCIM IDP integration + required: + - id + - enabled + - provider + - group_prefixes + - user_group_prefixes + - auth_token + - last_synced_at + properties: + id: + type: integer + format: int64 + description: The unique identifier for the integration + example: 123 + enabled: + type: boolean + description: Indicates whether the integration is enabled + example: true + provider: + type: string + description: Name of the SCIM identity provider + group_prefixes: + type: array + description: List of start_with string patterns for groups to sync + items: + type: string + example: [ "Engineering", "Sales" ] + user_group_prefixes: + type: array + description: List of start_with string patterns for groups which users to sync + items: + type: string + example: [ "Users" ] + auth_token: + type: string + description: SCIM API token (full on creation, masked otherwise) + example: "nbs_abc***********************************" + last_synced_at: + type: string + format: date-time + description: Timestamp of when the integration was last synced + example: "2023-05-15T10:30:00Z" + IdpIntegrationSyncLog: + type: object + description: Represents a synchronization log entry for an integration + required: + - id + - level + - timestamp + - message + properties: + id: + type: integer + format: int64 + description: The unique identifier for the sync log + example: 123 + level: + type: string + description: The log level + example: "info" + timestamp: + type: string + format: date-time + description: Timestamp of when the log was created + example: "2023-05-15T10:30:00Z" + message: + type: string + description: Log message + example: "Successfully synchronized users and groups" + ScimTokenResponse: + type: object + description: Response containing the regenerated SCIM token + required: + - auth_token + properties: + auth_token: + type: string + description: The newly generated SCIM API token + example: "nbs_F3f0d..." + BypassResponse: + type: object + description: Response for bypassed peer operations. + required: + - peer_id + properties: + peer_id: + type: string + description: The ID of the bypassed peer. + example: "chacbco6lnnbn6cg5s91" + ErrorResponse: + type: object + description: "Standard error response. Note: The exact structure of this error response is inferred from `util.WriteErrorResponse` and `util.WriteError` usage in the provided Go code, as a specific Go struct for errors was not provided." + properties: + message: + type: string + description: A human-readable error message. + example: "couldn't parse JSON request" responses: not_found: description: Resource not found @@ -2894,8 +3869,8 @@ paths: description: Returns version information for NetBird components including the current management server version and latest available versions from GitHub. tags: [ Instance ] security: - - BearerAuth: [] - - TokenAuth: [] + - BearerAuth: [ ] + - TokenAuth: [ ] responses: '200': description: Version information @@ -2942,8 +3917,8 @@ paths: description: Retrieve all jobs for a given peer tags: [ Jobs ] security: - - BearerAuth: [] - - TokenAuth: [] + - BearerAuth: [ ] + - TokenAuth: [ ] parameters: - in: path name: peerId @@ -2973,8 +3948,8 @@ paths: description: Create a new job for a given peer tags: [ Jobs ] security: - - BearerAuth: [] - - TokenAuth: [] + - BearerAuth: [ ] + - TokenAuth: [ ] parameters: - in: path name: peerId @@ -3010,8 +3985,8 @@ paths: description: Retrieve details of a specific job tags: [ Jobs ] security: - - BearerAuth: [] - - TokenAuth: [] + - BearerAuth: [ ] + - TokenAuth: [ ] parameters: - in: path name: peerId @@ -3401,7 +4376,7 @@ paths: responses: '200': description: Invite status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -3458,7 +4433,7 @@ paths: responses: '200': description: User rejected successfully - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -3492,7 +4467,7 @@ paths: responses: '200': description: Password changed successfully - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -3670,7 +4645,7 @@ paths: summary: Get invite information description: Retrieves public information about an invite. This endpoint is unauthenticated and protected by the token itself. tags: [ Users ] - security: [] + security: [ ] parameters: - in: path name: token @@ -3697,7 +4672,7 @@ paths: summary: Accept an invite description: Accepts an invite and creates the user with the provided password. This endpoint is unauthenticated and protected by the token itself. tags: [ Users ] - security: [] + security: [ ] parameters: - in: path name: token @@ -5971,21 +6946,21 @@ paths: required: false schema: type: string - enum: [TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP] + enum: [ TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP ] - name: connection_type in: query description: Filter by connection type required: false schema: type: string - enum: [P2P, ROUTED] + enum: [ P2P, ROUTED ] - name: direction in: query description: Filter by direction required: false schema: type: string - enum: [INGRESS, EGRESS, DIRECTION_UNKNOWN] + enum: [ INGRESS, EGRESS, DIRECTION_UNKNOWN ] - name: search in: query description: Case-insensitive partial match on user email, source/destination names, and source/destination addresses @@ -6356,3 +7331,1735 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/integrations/billing/usage: + get: + summary: Get current usage + tags: + - Usage + responses: + "200": + description: Current usage data + content: + application/json: + schema: + $ref: "#/components/schemas/UsageStats" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/subscription: + get: + summary: Get current subscription + tags: + - Subscription + responses: + "200": + description: Subscription details + content: + application/json: + schema: + $ref: "#/components/schemas/Subscription" + "401": + $ref: "#/components/responses/requires_authentication" + "404": + description: No subscription found + "500": + $ref: "#/components/responses/internal_error" + put: + summary: Change subscription + tags: + - Subscription + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + priceID: + type: string + description: The Price ID to change the subscription to. + example: "price_1HhxOpBzq4JbCqRmJxkpzL2V" + plan_tier: + type: string + description: The plan tier to change the subscription to. + example: business + responses: + "200": + description: Subscription successfully changed + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/plans: + get: + summary: Get available plans + tags: + - Plans + responses: + "200": + description: List of available plans + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Product" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/checkout: + post: + summary: Create checkout session + tags: + - Checkout + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + baseURL: + type: string + description: The base URL for the redirect after checkout. + example: "https://app.netbird.io/plans/success" + priceID: + type: string + description: The Price ID for checkout. + example: "price_1HhxOpBzq4JbCqRmJxkpzL2V" + enableTrial: + type: boolean + description: Enables a 14-day trial for the account. + required: + - baseURL + - priceID + responses: + "200": + description: Checkout session URL + content: + application/json: + schema: + $ref: "#/components/schemas/CheckoutResponse" + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/portal: + get: + summary: Get customer portal URL + tags: + - Portal + parameters: + - in: query + name: baseURL + schema: + type: string + required: true + description: The base URL for the redirect after accessing the portal. + example: "https://app.netbird.io/plans" + responses: + "200": + description: Customer portal URL + content: + application/json: + schema: + $ref: "#/components/schemas/PortalResponse" + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/invoices: + get: + summary: Get account's paid invoices + tags: + - Invoice + responses: + "200": + description: The account's paid invoices + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InvoiceResponse" + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/invoices/{id}/pdf: + get: + summary: Get account invoice URL to Stripe. + tags: + - Invoice + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of the invoice + responses: + "200": + description: The invoice URL to Stripe + content: + application/json: + schema: + $ref: "#/components/schemas/InvoicePDFResponse" + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/invoices/{id}/csv: + get: + summary: Get account invoice CSV. + tags: + - Invoice + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of the invoice + responses: + "200": + description: The invoice CSV + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename=in_1MtHbELkdIwHu7ixl4OzzPMv.csv + content: + text/csv: + schema: + type: string + example: | + description,qty,unit_price,amount + line item 2, 5, 1.00, 5.00 + line item 1, 10, 0.50, 5.00 + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/aws/marketplace/activate: + post: + summary: Activate AWS Marketplace subscription. + tags: + - AWS Marketplace + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + plan_tier: + type: string + description: The plan tier to activate the subscription for. + example: business + required: + - plan_tier + responses: + "200": + description: AWS subscription successfully activated + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/billing/aws/marketplace/enrich: + post: + summary: Enrich AWS Marketplace subscription with Account ID. + tags: + - AWS Marketplace + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + aws_user_id: + type: string + description: The AWS user ID. + example: eRF345hgdgFyu + required: + - aws_user_id + responses: + "200": + description: AWS subscription successfully enriched with Account ID. + "400": + $ref: "#/components/responses/bad_request" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/msp/tenants: + get: + summary: Get MSP tenants + tags: + - MSP + responses: + "200": + description: Get MSP tenants response + content: + application/json: + schema: + $ref: "#/components/schemas/GetTenantsResponse" + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + post: + summary: Create MSP tenant + tags: + - MSP + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateTenantRequest" + responses: + "200": + description: Create MSP tenant Response + content: + application/json: + schema: + $ref: "#/components/schemas/TenantResponse" + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/msp/tenants/{id}: + put: + summary: Update MSP tenant + tags: + - MSP + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of a tenant account + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateTenantRequest" + responses: + "200": + description: Update MSP tenant Response + content: + application/json: + schema: + $ref: "#/components/schemas/TenantResponse" + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/msp/tenants/{id}/unlink: + post: + summary: Unlink a tenant + tags: + - MSP + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of a tenant account + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + owner: + type: string + description: The new owners user ID. + example: "google-oauth2|123456789012345678901" + required: + - owner + responses: + "200": + description: Successfully unlinked the tenant + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "404": + description: The tenant was not found + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/msp/tenants/{id}/dns: + post: + summary: Verify a tenant domain DNS challenge + tags: + - MSP + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of a tenant account + responses: + "200": + description: Successfully verified the DNS challenge + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "404": + description: The tenant was not found + "500": + $ref: "#/components/responses/internal_error" + "501": + description: DNS Challenge Failed Response + content: + application/json: + schema: + $ref: "#/components/schemas/DNSChallengeResponse" + /api/integrations/msp/tenants/{id}/subscription: + post: + summary: Create subscription for Tenant + tags: + - MSP + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of a tenant account + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + priceID: + type: string + description: The Price ID to change the subscription to. + example: "price_1HhxOpBzq4JbCqRmJxkpzL2V" + required: + - priceID + responses: + "200": + description: Successfully created subscription for Tenant + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "404": + description: The tenant was not found + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/msp/tenants/{id}/invite: + post: + summary: Invite existing account as a Tenant to the MSP account + tags: + - MSP + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of an existing tenant account + responses: + "200": + description: Successfully invited existing Tenant to the MSP account + content: + application/json: + schema: + $ref: "#/components/schemas/TenantResponse" + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "404": + description: The tenant was not found + "500": + $ref: "#/components/responses/internal_error" + put: + summary: Response by the invited Tenant account owner + tags: + - MSP + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The unique identifier of an existing tenant account + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + value: + type: string + description: Accept or decline the invitation. + enum: + - accept + - decline + required: + - value + responses: + "200": + description: Successful response + "400": + $ref: "#/components/responses/bad_request" + "403": + $ref: "#/components/responses/requires_authentication" + "404": + description: The tenant was not found + "500": + $ref: "#/components/responses/internal_error" + /api/integrations/edr/intune: + post: + tags: + - EDR Intune Integrations + summary: Create EDR Intune Integration + description: | + Creates a new EDR Intune integration for the authenticated account. + operationId: createEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRIntuneRequest' + responses: + '200': + description: Integration created successfully. Returns the created integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRIntuneResponse' + '400': + description: Bad Request (e.g., invalid JSON, missing required fields, validation error). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized (e.g., missing or invalid authentication token). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - EDR Intune Integrations + summary: Get EDR Intune Integration + description: Retrieves a specific EDR Intune integration by its ID. + operationId: getEDRIntegration + responses: + '200': + description: Successfully retrieved the integration details. Config keys are masked. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRIntuneResponse' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found (e.g., integration with the given ID does not exist). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + tags: + - EDR Intune Integrations + summary: Update EDR Intune Integration + description: | + Updates an existing EDR Intune Integration. The request body structure is `EDRIntuneRequest`. + operationId: updateEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRIntuneRequest' + responses: + '200': + description: Integration updated successfully. Returns the updated integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRIntuneResponse' + '400': + description: Bad Request (e.g., invalid JSON, validation error, invalid ID). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - EDR Intune Integrations + summary: Delete EDR Intune Integration + description: Deletes an EDR Intune Integration by its ID. + operationId: deleteIntegration + responses: + '200': + description: Integration deleted successfully. Returns an empty object. + content: + application/json: + schema: + type: object + example: { } + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/integrations/edr/sentinelone: + post: + tags: + - EDR SentinelOne Integrations + summary: Create EDR SentinelOne Integration + description: Creates a new EDR SentinelOne integration + operationId: createSentinelOneEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRSentinelOneRequest' + responses: + '200': + description: Integration created successfully. Returns the created integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRSentinelOneResponse' + '400': + description: Bad Request (e.g., invalid JSON, missing required fields, validation error). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized (e.g., missing or invalid authentication token). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - EDR SentinelOne Integrations + summary: Get EDR SentinelOne Integration + description: Retrieves a specific EDR SentinelOne integration by its ID. + responses: + '200': + description: Successfully retrieved the integration details. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRSentinelOneResponse' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found (e.g., integration with the given ID does not exist). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + tags: + - EDR SentinelOne Integrations + summary: Update EDR SentinelOne Integration + description: Updates an existing EDR SentinelOne Integration. + operationId: updateSentinelOneEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRSentinelOneRequest' + responses: + '200': + description: Integration updated successfully. Returns the updated integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRSentinelOneResponse' + '400': + description: Bad Request (e.g., invalid JSON, validation error, invalid ID). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - EDR SentinelOne Integrations + summary: Delete EDR SentinelOne Integration + description: Deletes an EDR SentinelOne Integration by its ID. + responses: + '200': + description: Integration deleted successfully. Returns an empty object. + content: + application/json: + schema: + type: object + example: { } + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/integrations/edr/falcon: + post: + tags: + - EDR Falcon Integrations + summary: Create EDR Falcon Integration + description: Creates a new EDR Falcon integration + operationId: createFalconEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRFalconRequest' + responses: + '200': + description: Integration created successfully. Returns the created integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRFalconResponse' + '400': + description: Bad Request (e.g., invalid JSON, missing required fields, validation error). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized (e.g., missing or invalid authentication token). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - EDR Falcon Integrations + summary: Get EDR Falcon Integration + description: Retrieves a specific EDR Falcon integration by its ID. + responses: + '200': + description: Successfully retrieved the integration details. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRFalconResponse' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found (e.g., integration with the given ID does not exist). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + tags: + - EDR Falcon Integrations + summary: Update EDR Falcon Integration + description: Updates an existing EDR Falcon Integration. + operationId: updateFalconEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRFalconRequest' + responses: + '200': + description: Integration updated successfully. Returns the updated integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRFalconResponse' + '400': + description: Bad Request (e.g., invalid JSON, validation error, invalid ID). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - EDR Falcon Integrations + summary: Delete EDR Falcon Integration + description: Deletes an existing EDR Falcon Integration by its ID. + responses: + '202': + description: Integration deleted successfully. Typically returns no content. + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/integrations/scim-idp: + post: + tags: + - IDP + summary: Create SCIM IDP Integration + description: Creates a new SCIM integration + operationId: createSCIMIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScimIntegrationRequest' + responses: + '200': + description: Integration created successfully. Returns the created integration. + content: + application/json: + schema: + $ref: '#/components/schemas/ScimIntegration' + '400': + description: Bad Request (e.g., invalid JSON, missing required fields, validation error). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized (e.g., missing or invalid authentication token). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - IDP + summary: Get All SCIM IDP Integrations + description: Retrieves all SCIM IDP integrations for the authenticated account + operationId: getAllSCIMIntegrations + responses: + '200': + description: A list of SCIM IDP integrations. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ScimIntegration' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/integrations/scim-idp/{id}: + parameters: + - name: id + in: path + required: true + description: The unique identifier of the SCIM IDP integration. + schema: + type: string + example: "ch8i4ug6lnn4g9hqv7m0" + get: + tags: + - IDP + summary: Get SCIM IDP Integration + description: Retrieves an SCIM IDP integration by ID. + operationId: getSCIMIntegration + responses: + '200': + description: Successfully retrieved the integration details. + content: + application/json: + schema: + $ref: '#/components/schemas/ScimIntegration' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found (e.g., integration with the given ID does not exist). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + tags: + - IDP + summary: Update SCIM IDP Integration + description: Updates an existing SCIM IDP Integration. + operationId: updateSCIMIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScimIntegrationRequest' + responses: + '200': + description: Integration updated successfully. Returns the updated integration. + content: + application/json: + schema: + $ref: '#/components/schemas/ScimIntegration' + '400': + description: Bad Request (e.g., invalid JSON, validation error, invalid ID). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - IDP + summary: Delete SCIM IDP Integration + description: Deletes an SCIM IDP integration by ID. + operationId: deleteSCIMIntegration + responses: + '200': + description: Integration deleted successfully. Returns an empty object. + content: + application/json: + schema: + type: object + example: { } + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/integrations/scim-idp/{id}/token: + parameters: + - name: id + in: path + required: true + description: The unique identifier of the SCIM IDP integration. + schema: + type: string + example: "ch8i4ug6lnn4g9hqv7m0" + post: + tags: + - IDP + summary: Regenerate SCIM Token + description: Regenerates the SCIM API token for an SCIM IDP integration. + operationId: regenerateSCIMToken + responses: + '200': + description: Token regenerated successfully. Returns the new token. + content: + application/json: + schema: + $ref: '#/components/schemas/ScimTokenResponse' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/integrations/scim-idp/{id}/logs: + parameters: + - name: id + in: path + required: true + description: The unique identifier of the SCIM IDP integration. + schema: + type: string + example: "ch8i4ug6lnn4g9hqv7m0" + get: + tags: + - IDP + summary: Get SCIM Integration Sync Logs + description: Retrieves synchronization logs for a SCIM IDP integration. + operationId: getSCIMIntegrationLogs + responses: + '200': + description: Successfully retrieved the integration sync logs. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/IdpIntegrationSyncLog' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/integrations/edr/huntress: + post: + tags: + - EDR Huntress Integrations + summary: Create EDR Huntress Integration + description: Creates a new EDR Huntress integration + operationId: createHuntressEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRHuntressRequest' + responses: + '200': + description: Integration created successfully. Returns the created integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRHuntressResponse' + '400': + description: Bad Request (e.g., invalid JSON, missing required fields, validation error). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized (e.g., missing or invalid authentication token). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - EDR Huntress Integrations + summary: Get EDR Huntress Integration + description: Retrieves a specific EDR Huntress integration by its ID. + responses: + '200': + description: Successfully retrieved the integration details. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRHuntressResponse' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found (e.g., integration with the given ID does not exist). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + tags: + - EDR Huntress Integrations + summary: Update EDR Huntress Integration + description: Updates an existing EDR Huntress Integration. + operationId: updateHuntressEDRIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EDRHuntressRequest' + responses: + '200': + description: Integration updated successfully. Returns the updated integration. + content: + application/json: + schema: + $ref: '#/components/schemas/EDRHuntressResponse' + '400': + description: Bad Request (e.g., invalid JSON, validation error, invalid ID). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - EDR Huntress Integrations + summary: Delete EDR Huntress Integration + description: Deletes an EDR Huntress Integration by its ID. + responses: + '200': + description: Integration deleted successfully. Returns an empty object. + content: + application/json: + schema: + type: object + example: { } + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/peers/{peer-id}/edr/bypass: + parameters: + - name: peer-id + in: path + required: true + schema: + type: string + description: The unique identifier of the peer + post: + tags: + - EDR Peers + summary: Bypass compliance for a non-compliant peer + description: | + Allows an admin to bypass EDR compliance checks for a specific peer. + The peer will remain bypassed until the admin revokes it OR the device becomes + naturally compliant in the EDR system. + operationId: bypassCompliance + responses: + '200': + description: Peer compliance bypassed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BypassResponse' + '400': + description: Bad Request (peer not in non-compliant state) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - EDR Peers + summary: Revoke compliance bypass for a peer + description: Removes the compliance bypass, subjecting the peer to normal EDR validation. + operationId: revokeBypass + responses: + '200': + description: Compliance bypass revoked successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/peers/edr/bypassed: + get: + tags: + - EDR Peers + summary: List all bypassed peers + description: Returns all peers that have compliance bypassed by an admin. + operationId: listBypassedPeers + responses: + '200': + description: List of bypassed peers + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BypassResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/event-streaming: + post: + tags: + - Event Streaming Integrations + summary: Create Event Streaming Integration + description: | + Creates a new event streaming integration for the authenticated account. + The request body should conform to `CreateIntegrationRequest`. + Note: Based on the provided Go code, the `enabled` field from the request is part of the `CreateIntegrationRequest` struct, + but the backend `manager.CreateIntegration` function signature shown does not directly use this `enabled` field. + The actual behavior for `enabled` during creation should be confirmed (e.g., it might have a server-side default or be handled by other logic). + operationId: createIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateIntegrationRequest' + responses: + '200': + description: Integration created successfully. Returns the created integration. + content: + application/json: + schema: + $ref: '#/components/schemas/IntegrationResponse' + '400': + description: Bad Request (e.g., invalid JSON, missing required fields, validation error). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized (e.g., missing or invalid authentication token). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Event Streaming Integrations + summary: List Event Streaming Integrations + description: Retrieves all event streaming integrations for the authenticated account. + operationId: getAllIntegrations + responses: + '200': + description: A list of event streaming integrations. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/IntegrationResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/event-streaming/{id}: + parameters: + - name: id + in: path + required: true + description: The unique numeric identifier of the event streaming integration. + schema: + type: integer + example: 123 + get: + tags: + - Event Streaming Integrations + summary: Get Event Streaming Integration + description: Retrieves a specific event streaming integration by its ID. + operationId: getIntegration + responses: + '200': + description: Successfully retrieved the integration details. Config keys are masked. + content: + application/json: + schema: + $ref: '#/components/schemas/IntegrationResponse' + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found (e.g., integration with the given ID does not exist). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + tags: + - Event Streaming Integrations + summary: Update Event Streaming Integration + description: | + Updates an existing event streaming integration. The request body structure is `CreateIntegrationRequest`. + However, for updates: + - The `platform` field, if provided in the body, is ignored by the backend manager function, as the platform of an existing integration is typically immutable. + - The `enabled` and `config` fields from the request body are used to update the integration. + operationId: updateIntegration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateIntegrationRequest' + responses: + '200': + description: Integration updated successfully. Returns the updated integration. + content: + application/json: + schema: + $ref: '#/components/schemas/IntegrationResponse' + '400': + description: Bad Request (e.g., invalid JSON, validation error, invalid ID). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - Event Streaming Integrations + summary: Delete Event Streaming Integration + description: Deletes an event streaming integration by its ID. + operationId: deleteIntegration + responses: + '200': + description: Integration deleted successfully. Returns an empty object. + content: + application/json: + schema: + type: object + example: { } + '400': + description: Bad Request (e.g., invalid integration ID format). + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not Found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal Server Error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index fd7c61917..3f16af46b 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -16,6 +16,14 @@ const ( TokenAuthScopes = "TokenAuth.Scopes" ) +// Defines values for CreateIntegrationRequestPlatform. +const ( + CreateIntegrationRequestPlatformDatadog CreateIntegrationRequestPlatform = "datadog" + CreateIntegrationRequestPlatformFirehose CreateIntegrationRequestPlatform = "firehose" + CreateIntegrationRequestPlatformGenericHttp CreateIntegrationRequestPlatform = "generic_http" + CreateIntegrationRequestPlatformS3 CreateIntegrationRequestPlatform = "s3" +) + // Defines values for DNSRecordType. const ( DNSRecordTypeA DNSRecordType = "A" @@ -188,6 +196,20 @@ const ( IngressPortAllocationRequestPortRangeProtocolUdp IngressPortAllocationRequestPortRangeProtocol = "udp" ) +// Defines values for IntegrationResponsePlatform. +const ( + IntegrationResponsePlatformDatadog IntegrationResponsePlatform = "datadog" + IntegrationResponsePlatformFirehose IntegrationResponsePlatform = "firehose" + IntegrationResponsePlatformGenericHttp IntegrationResponsePlatform = "generic_http" + IntegrationResponsePlatformS3 IntegrationResponsePlatform = "s3" +) + +// Defines values for InvoiceResponseType. +const ( + InvoiceResponseTypeAccount InvoiceResponseType = "account" + InvoiceResponseTypeTenants InvoiceResponseType = "tenants" +) + // Defines values for JobResponseStatus. const ( JobResponseStatusFailed JobResponseStatus = "failed" @@ -266,6 +288,21 @@ const ( ResourceTypeSubnet ResourceType = "subnet" ) +// Defines values for SentinelOneMatchAttributesNetworkStatus. +const ( + SentinelOneMatchAttributesNetworkStatusConnected SentinelOneMatchAttributesNetworkStatus = "connected" + SentinelOneMatchAttributesNetworkStatusDisconnected SentinelOneMatchAttributesNetworkStatus = "disconnected" + SentinelOneMatchAttributesNetworkStatusQuarantined SentinelOneMatchAttributesNetworkStatus = "quarantined" +) + +// Defines values for TenantResponseStatus. +const ( + TenantResponseStatusActive TenantResponseStatus = "active" + TenantResponseStatusExisting TenantResponseStatus = "existing" + TenantResponseStatusInvited TenantResponseStatus = "invited" + TenantResponseStatusPending TenantResponseStatus = "pending" +) + // Defines values for UserStatus. const ( UserStatusActive UserStatus = "active" @@ -299,6 +336,12 @@ const ( GetApiEventsNetworkTrafficParamsDirectionINGRESS GetApiEventsNetworkTrafficParamsDirection = "INGRESS" ) +// Defines values for PutApiIntegrationsMspTenantsIdInviteJSONBodyValue. +const ( + PutApiIntegrationsMspTenantsIdInviteJSONBodyValueAccept PutApiIntegrationsMspTenantsIdInviteJSONBodyValue = "accept" + PutApiIntegrationsMspTenantsIdInviteJSONBodyValueDecline PutApiIntegrationsMspTenantsIdInviteJSONBodyValue = "decline" +) + // AccessiblePeer defines model for AccessiblePeer. type AccessiblePeer struct { // CityName Commonly used English name of the city @@ -490,6 +533,21 @@ type BundleWorkloadResponse struct { Type WorkloadType `json:"type"` } +// BypassResponse Response for bypassed peer operations. +type BypassResponse struct { + // PeerId The ID of the bypassed peer. + PeerId string `json:"peer_id"` +} + +// CheckoutResponse defines model for CheckoutResponse. +type CheckoutResponse struct { + // SessionId The unique identifier for the checkout session. + SessionId string `json:"session_id"` + + // Url URL to redirect the user to the checkout session. + Url string `json:"url"` +} + // Checks List of objects that perform the actual checks type Checks struct { // GeoLocationCheck Posture check for geo location @@ -532,6 +590,36 @@ type Country struct { // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country type CountryCode = string +// CreateIntegrationRequest Request payload for creating a new event streaming integration. Also used as the structure for the PUT request body, but not all fields are applicable for updates (see PUT operation description). +type CreateIntegrationRequest struct { + // Config Platform-specific configuration as key-value pairs. For creation, all necessary credentials and settings must be provided. For updates, provide the fields to change or the entire new configuration. + Config map[string]string `json:"config"` + + // Enabled Specifies whether the integration is enabled. During creation (POST), this value is sent by the client, but the provided backend manager function `CreateIntegration` does not appear to use it directly, so its effect on creation should be verified. During updates (PUT), this field is used to enable or disable the integration. + Enabled bool `json:"enabled"` + + // Platform The event streaming platform to integrate with (e.g., "datadog", "s3", "firehose"). This field is used for creation. For updates (PUT), this field, if sent, is ignored by the backend. + Platform CreateIntegrationRequestPlatform `json:"platform"` +} + +// CreateIntegrationRequestPlatform The event streaming platform to integrate with (e.g., "datadog", "s3", "firehose"). This field is used for creation. For updates (PUT), this field, if sent, is ignored by the backend. +type CreateIntegrationRequestPlatform string + +// CreateScimIntegrationRequest Request payload for creating an SCIM IDP integration +type CreateScimIntegrationRequest struct { + // GroupPrefixes List of start_with string patterns for groups to sync + GroupPrefixes *[]string `json:"group_prefixes,omitempty"` + + // Prefix The connection prefix used for the SCIM provider + Prefix string `json:"prefix"` + + // Provider Name of the SCIM identity provider + Provider string `json:"provider"` + + // UserGroupPrefixes List of start_with string patterns for groups which users to sync + UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"` +} + // CreateSetupKeyRequest defines model for CreateSetupKeyRequest. type CreateSetupKeyRequest struct { // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer @@ -556,6 +644,24 @@ type CreateSetupKeyRequest struct { UsageLimit int `json:"usage_limit"` } +// CreateTenantRequest defines model for CreateTenantRequest. +type CreateTenantRequest struct { + // Domain The name for the MSP tenant + Domain string `json:"domain"` + + // Groups MSP users Groups that can access the Tenant and Roles to assume + Groups []TenantGroupResponse `json:"groups"` + + // Name The name for the MSP tenant + Name string `json:"name"` +} + +// DNSChallengeResponse defines model for DNSChallengeResponse. +type DNSChallengeResponse struct { + // DnsChallenge The DNS challenge to set in a TXT record + DnsChallenge string `json:"dns_challenge"` +} + // DNSRecord defines model for DNSRecord. type DNSRecord struct { // Content DNS record content (IP address for A/AAAA, domain for CNAME) @@ -598,6 +704,234 @@ type DNSSettings struct { DisabledManagementGroups []string `json:"disabled_management_groups"` } +// EDRFalconRequest Request payload for creating or updating a EDR Falcon integration +type EDRFalconRequest struct { + // ClientId CrowdStrike API client ID + ClientId string `json:"client_id"` + + // CloudId CrowdStrike cloud identifier (e.g., "us-1", "us-2", "eu-1") + CloudId string `json:"cloud_id"` + + // Enabled Indicates whether the integration is enabled + Enabled *bool `json:"enabled,omitempty"` + + // Groups The Groups this integration applies to + Groups []string `json:"groups"` + + // Secret CrowdStrike API client secret + Secret string `json:"secret"` + + // ZtaScoreThreshold The minimum Zero Trust Assessment score required for agent approval (0-100) + ZtaScoreThreshold int `json:"zta_score_threshold"` +} + +// EDRFalconResponse Represents a Falcon EDR integration +type EDRFalconResponse struct { + // AccountId The identifier of the account this integration belongs to. + AccountId string `json:"account_id"` + + // CloudId CrowdStrike cloud identifier + CloudId string `json:"cloud_id"` + + // CreatedAt Timestamp of when the integration was created. + CreatedAt time.Time `json:"created_at"` + + // CreatedBy The user id that created the integration + CreatedBy string `json:"created_by"` + + // Enabled Indicates whether the integration is enabled + Enabled bool `json:"enabled"` + + // Groups List of groups + Groups []Group `json:"groups"` + + // Id The unique numeric identifier for the integration. + Id int64 `json:"id"` + + // LastSyncedAt Timestamp of when the integration was last synced. + LastSyncedAt time.Time `json:"last_synced_at"` + + // UpdatedAt Timestamp of when the integration was last updated. + UpdatedAt time.Time `json:"updated_at"` + + // ZtaScoreThreshold The minimum Zero Trust Assessment score required for agent approval (0-100) + ZtaScoreThreshold int `json:"zta_score_threshold"` +} + +// EDRHuntressRequest Request payload for creating or updating a EDR Huntress integration +type EDRHuntressRequest struct { + // ApiKey Huntress API key + ApiKey string `json:"api_key"` + + // ApiSecret Huntress API secret + ApiSecret string `json:"api_secret"` + + // Enabled Indicates whether the integration is enabled + Enabled *bool `json:"enabled,omitempty"` + + // Groups The Groups this integrations applies to + Groups []string `json:"groups"` + + // LastSyncedInterval The devices last sync requirement interval in hours. Minimum value is 24 hours + LastSyncedInterval int `json:"last_synced_interval"` + + // MatchAttributes Attribute conditions to match when approving agents + MatchAttributes HuntressMatchAttributes `json:"match_attributes"` +} + +// EDRHuntressResponse Represents a Huntress EDR integration configuration +type EDRHuntressResponse struct { + // AccountId The identifier of the account this integration belongs to. + AccountId string `json:"account_id"` + + // CreatedAt Timestamp of when the integration was created. + CreatedAt time.Time `json:"created_at"` + + // CreatedBy The user id that created the integration + CreatedBy string `json:"created_by"` + + // Enabled Indicates whether the integration is enabled + Enabled bool `json:"enabled"` + + // Groups List of groups + Groups []Group `json:"groups"` + + // Id The unique numeric identifier for the integration. + Id int64 `json:"id"` + + // LastSyncedAt Timestamp of when the integration was last synced. + LastSyncedAt time.Time `json:"last_synced_at"` + + // LastSyncedInterval The devices last sync requirement interval in hours. + LastSyncedInterval int `json:"last_synced_interval"` + + // MatchAttributes Attribute conditions to match when approving agents + MatchAttributes HuntressMatchAttributes `json:"match_attributes"` + + // UpdatedAt Timestamp of when the integration was last updated. + UpdatedAt time.Time `json:"updated_at"` +} + +// EDRIntuneRequest Request payload for creating or updating a EDR Intune integration. +type EDRIntuneRequest struct { + // ClientId The Azure application client id + ClientId string `json:"client_id"` + + // Enabled Indicates whether the integration is enabled + Enabled *bool `json:"enabled,omitempty"` + + // Groups The Groups this integrations applies to + Groups []string `json:"groups"` + + // LastSyncedInterval The devices last sync requirement interval in hours. Minimum value is 24 hours. + LastSyncedInterval int `json:"last_synced_interval"` + + // Secret The Azure application client secret + Secret string `json:"secret"` + + // TenantId The Azure tenant id + TenantId string `json:"tenant_id"` +} + +// EDRIntuneResponse Represents a Intune EDR integration configuration +type EDRIntuneResponse struct { + // AccountId The identifier of the account this integration belongs to. + AccountId string `json:"account_id"` + + // ClientId The Azure application client id + ClientId string `json:"client_id"` + + // CreatedAt Timestamp of when the integration was created. + CreatedAt time.Time `json:"created_at"` + + // CreatedBy The user id that created the integration + CreatedBy string `json:"created_by"` + + // Enabled Indicates whether the integration is enabled + Enabled bool `json:"enabled"` + + // Groups List of groups + Groups []Group `json:"groups"` + + // Id The unique numeric identifier for the integration. + Id int64 `json:"id"` + + // LastSyncedAt Timestamp of when the integration was last synced. + LastSyncedAt time.Time `json:"last_synced_at"` + + // LastSyncedInterval The devices last sync requirement interval in hours. + LastSyncedInterval int `json:"last_synced_interval"` + + // TenantId The Azure tenant id + TenantId string `json:"tenant_id"` + + // UpdatedAt Timestamp of when the integration was last updated. + UpdatedAt time.Time `json:"updated_at"` +} + +// EDRSentinelOneRequest Request payload for creating or updating a EDR SentinelOne integration +type EDRSentinelOneRequest struct { + // ApiToken SentinelOne API token + ApiToken string `json:"api_token"` + + // ApiUrl The Base URL of SentinelOne API + ApiUrl string `json:"api_url"` + + // Enabled Indicates whether the integration is enabled + Enabled *bool `json:"enabled,omitempty"` + + // Groups The Groups this integrations applies to + Groups []string `json:"groups"` + + // LastSyncedInterval The devices last sync requirement interval in hours. Minimum value is 24 hours. + LastSyncedInterval int `json:"last_synced_interval"` + + // MatchAttributes Attribute conditions to match when approving agents + MatchAttributes SentinelOneMatchAttributes `json:"match_attributes"` +} + +// EDRSentinelOneResponse Represents a SentinelOne EDR integration configuration +type EDRSentinelOneResponse struct { + // AccountId The identifier of the account this integration belongs to. + AccountId string `json:"account_id"` + + // ApiUrl The Base URL of SentinelOne API + ApiUrl string `json:"api_url"` + + // CreatedAt Timestamp of when the integration was created. + CreatedAt time.Time `json:"created_at"` + + // CreatedBy The user id that created the integration + CreatedBy string `json:"created_by"` + + // Enabled Indicates whether the integration is enabled + Enabled bool `json:"enabled"` + + // Groups List of groups + Groups []Group `json:"groups"` + + // Id The unique numeric identifier for the integration. + Id int64 `json:"id"` + + // LastSyncedAt Timestamp of when the integration was last synced. + LastSyncedAt time.Time `json:"last_synced_at"` + + // LastSyncedInterval The devices last sync requirement interval in hours. + LastSyncedInterval int `json:"last_synced_interval"` + + // MatchAttributes Attribute conditions to match when approving agents + MatchAttributes SentinelOneMatchAttributes `json:"match_attributes"` + + // UpdatedAt Timestamp of when the integration was last updated. + UpdatedAt time.Time `json:"updated_at"` +} + +// ErrorResponse Standard error response. Note: The exact structure of this error response is inferred from `util.WriteErrorResponse` and `util.WriteError` usage in the provided Go code, as a specific Go struct for errors was not provided. +type ErrorResponse struct { + // Message A human-readable error message. + Message *string `json:"message,omitempty"` +} + // Event defines model for Event. type Event struct { // Activity The activity that occurred during the event @@ -643,6 +977,9 @@ type GeoLocationCheck struct { // GeoLocationCheckAction Action to take upon policy match type GeoLocationCheckAction string +// GetTenantsResponse defines model for GetTenantsResponse. +type GetTenantsResponse = []TenantResponse + // Group defines model for Group. type Group struct { // Id Group ID @@ -699,6 +1036,21 @@ type GroupRequest struct { Resources *[]Resource `json:"resources,omitempty"` } +// HuntressMatchAttributes Attribute conditions to match when approving agents +type HuntressMatchAttributes struct { + // DefenderPolicyStatus Policy status of Defender AV for Managed Antivirus. + DefenderPolicyStatus *string `json:"defender_policy_status,omitempty"` + + // DefenderStatus Status of Defender AV Managed Antivirus. + DefenderStatus *string `json:"defender_status,omitempty"` + + // DefenderSubstatus Sub-status of Defender AV Managed Antivirus. + DefenderSubstatus *string `json:"defender_substatus,omitempty"` + + // FirewallStatus Status of agent firewall. Can be one of Disabled, Enabled, Pending Isolation, Isolated, Pending Release. + FirewallStatus *string `json:"firewall_status,omitempty"` +} + // IdentityProvider defines model for IdentityProvider. type IdentityProvider struct { // ClientId OAuth2 client ID @@ -738,6 +1090,21 @@ type IdentityProviderRequest struct { // IdentityProviderType Type of identity provider type IdentityProviderType string +// IdpIntegrationSyncLog Represents a synchronization log entry for an integration +type IdpIntegrationSyncLog struct { + // Id The unique identifier for the sync log + Id int64 `json:"id"` + + // Level The log level + Level string `json:"level"` + + // Message Log message + Message string `json:"message"` + + // Timestamp Timestamp of when the log was created + Timestamp time.Time `json:"timestamp"` +} + // IngressPeer defines model for IngressPeer. type IngressPeer struct { AvailablePorts AvailablePorts `json:"available_ports"` @@ -892,6 +1259,57 @@ type InstanceVersionInfo struct { ManagementUpdateAvailable bool `json:"management_update_available"` } +// IntegrationResponse Represents an event streaming integration. +type IntegrationResponse struct { + // AccountId The identifier of the account this integration belongs to. + AccountId *string `json:"account_id,omitempty"` + + // Config Configuration for the integration. Sensitive keys (like API keys, secret keys) are masked with '****' in responses, as indicated by the GetIntegration handler logic. + Config *map[string]string `json:"config,omitempty"` + + // CreatedAt Timestamp of when the integration was created. + CreatedAt *time.Time `json:"created_at,omitempty"` + + // Enabled Whether the integration is currently active. + Enabled *bool `json:"enabled,omitempty"` + + // Id The unique numeric identifier for the integration. + Id *int64 `json:"id,omitempty"` + + // Platform The event streaming platform. + Platform *IntegrationResponsePlatform `json:"platform,omitempty"` + + // UpdatedAt Timestamp of when the integration was last updated. + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +// IntegrationResponsePlatform The event streaming platform. +type IntegrationResponsePlatform string + +// InvoicePDFResponse defines model for InvoicePDFResponse. +type InvoicePDFResponse struct { + // Url URL to redirect the user to invoice. + Url string `json:"url"` +} + +// InvoiceResponse defines model for InvoiceResponse. +type InvoiceResponse struct { + // Id The Stripe invoice id + Id string `json:"id"` + + // PeriodEnd The end date of the invoice period. + PeriodEnd time.Time `json:"period_end"` + + // PeriodStart The start date of the invoice period. + PeriodStart time.Time `json:"period_start"` + + // Type The invoice type + Type InvoiceResponseType `json:"type"` +} + +// InvoiceResponseType The invoice type +type InvoiceResponseType string + // JobRequest defines model for JobRequest. type JobRequest struct { Workload WorkloadRequest `json:"workload"` @@ -1797,6 +2215,15 @@ type PolicyUpdate struct { SourcePostureChecks *[]string `json:"source_posture_checks,omitempty"` } +// PortalResponse defines model for PortalResponse. +type PortalResponse struct { + // SessionId The unique identifier for the customer portal session. + SessionId string `json:"session_id"` + + // Url URL to redirect the user to the customer portal. + Url string `json:"url"` +} + // PostureCheck defines model for PostureCheck. type PostureCheck struct { // Checks List of objects that perform the actual checks @@ -1824,6 +2251,21 @@ type PostureCheckUpdate struct { Name string `json:"name"` } +// Price defines model for Price. +type Price struct { + // Currency Currency code for this price. + Currency string `json:"currency"` + + // Price Price amount in minor units (e.g., cents). + Price int `json:"price"` + + // PriceId Unique identifier for the price. + PriceId string `json:"price_id"` + + // Unit Unit of measurement for this price (e.g., per user). + Unit string `json:"unit"` +} + // Process Describes the operational activity within a peer's system. type Process struct { // LinuxPath Path to the process executable file in a Linux operating system @@ -1841,6 +2283,24 @@ type ProcessCheck struct { Processes []Process `json:"processes"` } +// Product defines model for Product. +type Product struct { + // Description Detailed description of the product. + Description string `json:"description"` + + // Features List of features provided by the product. + Features []string `json:"features"` + + // Free Indicates whether the product is free or not. + Free bool `json:"free"` + + // Name Name of the product. + Name string `json:"name"` + + // Prices List of prices for the product in different currencies + Prices []Price `json:"prices"` +} + // Resource defines model for Resource. type Resource struct { // Id ID of the resource @@ -1950,6 +2410,66 @@ type RulePortRange struct { Start int `json:"start"` } +// ScimIntegration Represents a SCIM IDP integration +type ScimIntegration struct { + // AuthToken SCIM API token (full on creation, masked otherwise) + AuthToken string `json:"auth_token"` + + // Enabled Indicates whether the integration is enabled + Enabled bool `json:"enabled"` + + // GroupPrefixes List of start_with string patterns for groups to sync + GroupPrefixes []string `json:"group_prefixes"` + + // Id The unique identifier for the integration + Id int64 `json:"id"` + + // LastSyncedAt Timestamp of when the integration was last synced + LastSyncedAt time.Time `json:"last_synced_at"` + + // Provider Name of the SCIM identity provider + Provider string `json:"provider"` + + // UserGroupPrefixes List of start_with string patterns for groups which users to sync + UserGroupPrefixes []string `json:"user_group_prefixes"` +} + +// ScimTokenResponse Response containing the regenerated SCIM token +type ScimTokenResponse struct { + // AuthToken The newly generated SCIM API token + AuthToken string `json:"auth_token"` +} + +// SentinelOneMatchAttributes Attribute conditions to match when approving agents +type SentinelOneMatchAttributes struct { + // ActiveThreats The maximum allowed number of active threats on the agent + ActiveThreats *int `json:"active_threats,omitempty"` + + // EncryptedApplications Whether disk encryption is enabled on the agent + EncryptedApplications *bool `json:"encrypted_applications,omitempty"` + + // FirewallEnabled Whether the agent firewall is enabled + FirewallEnabled *bool `json:"firewall_enabled,omitempty"` + + // Infected Whether the agent is currently flagged as infected + Infected *bool `json:"infected,omitempty"` + + // IsActive Whether the agent has been recently active and reporting + IsActive *bool `json:"is_active,omitempty"` + + // IsUpToDate Whether the agent is running the latest available version + IsUpToDate *bool `json:"is_up_to_date,omitempty"` + + // NetworkStatus The current network connectivity status of the device + NetworkStatus *SentinelOneMatchAttributesNetworkStatus `json:"network_status,omitempty"` + + // OperationalState The current operational state of the agent + OperationalState *string `json:"operational_state,omitempty"` +} + +// SentinelOneMatchAttributesNetworkStatus The current network connectivity status of the device +type SentinelOneMatchAttributesNetworkStatus string + // SetupKey defines model for SetupKey. type SetupKey struct { // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer @@ -2121,6 +2641,117 @@ type SetupResponse struct { UserId string `json:"user_id"` } +// Subscription defines model for Subscription. +type Subscription struct { + // Active Indicates whether the subscription is active or not. + Active bool `json:"active"` + + // Currency Currency code of the subscription. + Currency string `json:"currency"` + + // Features List of features included in the subscription. + Features *[]string `json:"features,omitempty"` + + // PlanTier The tier of the plan for the subscription. + PlanTier string `json:"plan_tier"` + + // Price Price amount in minor units (e.g., cents). + Price int `json:"price"` + + // PriceId Unique identifier for the price of the subscription. + PriceId string `json:"price_id"` + + // Provider The provider of the subscription. + Provider string `json:"provider"` + + // RemainingTrial The remaining time for the trial period, in seconds. + RemainingTrial *int `json:"remaining_trial,omitempty"` + + // UpdatedAt The date and time when the subscription was last updated. + UpdatedAt time.Time `json:"updated_at"` +} + +// TenantGroupResponse defines model for TenantGroupResponse. +type TenantGroupResponse struct { + // Id The Group ID + Id string `json:"id"` + + // Role The Role name + Role string `json:"role"` +} + +// TenantResponse defines model for TenantResponse. +type TenantResponse struct { + // ActivatedAt The date and time when the tenant was activated. + ActivatedAt *time.Time `json:"activated_at,omitempty"` + + // CreatedAt The date and time when the tenant was created. + CreatedAt time.Time `json:"created_at"` + + // DnsChallenge The DNS challenge to set in a TXT record + DnsChallenge string `json:"dns_challenge"` + + // Domain The tenant account domain + Domain string `json:"domain"` + + // Groups MSP users Groups that can access the Tenant and Roles to assume + Groups []TenantGroupResponse `json:"groups"` + + // Id The updated MSP tenant account ID + Id string `json:"id"` + + // InvitedAt The date and time when the existing tenant was invited. + InvitedAt *time.Time `json:"invited_at,omitempty"` + + // Name The name for the MSP tenant + Name string `json:"name"` + + // Status The status of the tenant + Status TenantResponseStatus `json:"status"` + + // UpdatedAt The date and time when the tenant was last updated. + UpdatedAt time.Time `json:"updated_at"` +} + +// TenantResponseStatus The status of the tenant +type TenantResponseStatus string + +// UpdateScimIntegrationRequest Request payload for updating an SCIM IDP integration +type UpdateScimIntegrationRequest struct { + // Enabled Indicates whether the integration is enabled + Enabled *bool `json:"enabled,omitempty"` + + // GroupPrefixes List of start_with string patterns for groups to sync + GroupPrefixes *[]string `json:"group_prefixes,omitempty"` + + // UserGroupPrefixes List of start_with string patterns for groups which users to sync + UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"` +} + +// UpdateTenantRequest defines model for UpdateTenantRequest. +type UpdateTenantRequest struct { + // Groups MSP users Groups that can access the Tenant and Roles to assume + Groups []TenantGroupResponse `json:"groups"` + + // Name The name for the MSP tenant + Name string `json:"name"` +} + +// UsageStats defines model for UsageStats. +type UsageStats struct { + // ActivePeers Number of active peers. + ActivePeers int64 `json:"active_peers"` + + // ActiveUsers Number of active users. + ActiveUsers int64 `json:"active_users"` + + // TotalPeers Total number of peers. + TotalPeers int64 `json:"total_peers"` + + // TotalUsers Total number of users. + TotalUsers int64 `json:"total_users"` +} + // User defines model for User. type User struct { // AutoGroups Group IDs to auto-assign to peers registered by this user @@ -2407,6 +3038,66 @@ type GetApiGroupsParams struct { Name *string `form:"name,omitempty" json:"name,omitempty"` } +// PostApiIntegrationsBillingAwsMarketplaceActivateJSONBody defines parameters for PostApiIntegrationsBillingAwsMarketplaceActivate. +type PostApiIntegrationsBillingAwsMarketplaceActivateJSONBody struct { + // PlanTier The plan tier to activate the subscription for. + PlanTier string `json:"plan_tier"` +} + +// PostApiIntegrationsBillingAwsMarketplaceEnrichJSONBody defines parameters for PostApiIntegrationsBillingAwsMarketplaceEnrich. +type PostApiIntegrationsBillingAwsMarketplaceEnrichJSONBody struct { + // AwsUserId The AWS user ID. + AwsUserId string `json:"aws_user_id"` +} + +// PostApiIntegrationsBillingCheckoutJSONBody defines parameters for PostApiIntegrationsBillingCheckout. +type PostApiIntegrationsBillingCheckoutJSONBody struct { + // BaseURL The base URL for the redirect after checkout. + BaseURL string `json:"baseURL"` + + // EnableTrial Enables a 14-day trial for the account. + EnableTrial *bool `json:"enableTrial,omitempty"` + + // PriceID The Price ID for checkout. + PriceID string `json:"priceID"` +} + +// GetApiIntegrationsBillingPortalParams defines parameters for GetApiIntegrationsBillingPortal. +type GetApiIntegrationsBillingPortalParams struct { + // BaseURL The base URL for the redirect after accessing the portal. + BaseURL string `form:"baseURL" json:"baseURL"` +} + +// PutApiIntegrationsBillingSubscriptionJSONBody defines parameters for PutApiIntegrationsBillingSubscription. +type PutApiIntegrationsBillingSubscriptionJSONBody struct { + // PlanTier The plan tier to change the subscription to. + PlanTier *string `json:"plan_tier,omitempty"` + + // PriceID The Price ID to change the subscription to. + PriceID *string `json:"priceID,omitempty"` +} + +// PutApiIntegrationsMspTenantsIdInviteJSONBody defines parameters for PutApiIntegrationsMspTenantsIdInvite. +type PutApiIntegrationsMspTenantsIdInviteJSONBody struct { + // Value Accept or decline the invitation. + Value PutApiIntegrationsMspTenantsIdInviteJSONBodyValue `json:"value"` +} + +// PutApiIntegrationsMspTenantsIdInviteJSONBodyValue defines parameters for PutApiIntegrationsMspTenantsIdInvite. +type PutApiIntegrationsMspTenantsIdInviteJSONBodyValue string + +// PostApiIntegrationsMspTenantsIdSubscriptionJSONBody defines parameters for PostApiIntegrationsMspTenantsIdSubscription. +type PostApiIntegrationsMspTenantsIdSubscriptionJSONBody struct { + // PriceID The Price ID to change the subscription to. + PriceID string `json:"priceID"` +} + +// PostApiIntegrationsMspTenantsIdUnlinkJSONBody defines parameters for PostApiIntegrationsMspTenantsIdUnlink. +type PostApiIntegrationsMspTenantsIdUnlinkJSONBody struct { + // Owner The new owners user ID. + Owner string `json:"owner"` +} + // GetApiPeersParams defines parameters for GetApiPeers. type GetApiPeersParams struct { // Name Filter peers by name @@ -2452,6 +3143,12 @@ type PostApiDnsZonesZoneIdRecordsJSONRequestBody = DNSRecordRequest // PutApiDnsZonesZoneIdRecordsRecordIdJSONRequestBody defines body for PutApiDnsZonesZoneIdRecordsRecordId for application/json ContentType. type PutApiDnsZonesZoneIdRecordsRecordIdJSONRequestBody = DNSRecordRequest +// CreateIntegrationJSONRequestBody defines body for CreateIntegration for application/json ContentType. +type CreateIntegrationJSONRequestBody = CreateIntegrationRequest + +// UpdateIntegrationJSONRequestBody defines body for UpdateIntegration for application/json ContentType. +type UpdateIntegrationJSONRequestBody = CreateIntegrationRequest + // PostApiGroupsJSONRequestBody defines body for PostApiGroups for application/json ContentType. type PostApiGroupsJSONRequestBody = GroupRequest @@ -2470,6 +3167,63 @@ type PostApiIngressPeersJSONRequestBody = IngressPeerCreateRequest // PutApiIngressPeersIngressPeerIdJSONRequestBody defines body for PutApiIngressPeersIngressPeerId for application/json ContentType. type PutApiIngressPeersIngressPeerIdJSONRequestBody = IngressPeerUpdateRequest +// PostApiIntegrationsBillingAwsMarketplaceActivateJSONRequestBody defines body for PostApiIntegrationsBillingAwsMarketplaceActivate for application/json ContentType. +type PostApiIntegrationsBillingAwsMarketplaceActivateJSONRequestBody PostApiIntegrationsBillingAwsMarketplaceActivateJSONBody + +// PostApiIntegrationsBillingAwsMarketplaceEnrichJSONRequestBody defines body for PostApiIntegrationsBillingAwsMarketplaceEnrich for application/json ContentType. +type PostApiIntegrationsBillingAwsMarketplaceEnrichJSONRequestBody PostApiIntegrationsBillingAwsMarketplaceEnrichJSONBody + +// PostApiIntegrationsBillingCheckoutJSONRequestBody defines body for PostApiIntegrationsBillingCheckout for application/json ContentType. +type PostApiIntegrationsBillingCheckoutJSONRequestBody PostApiIntegrationsBillingCheckoutJSONBody + +// PutApiIntegrationsBillingSubscriptionJSONRequestBody defines body for PutApiIntegrationsBillingSubscription for application/json ContentType. +type PutApiIntegrationsBillingSubscriptionJSONRequestBody PutApiIntegrationsBillingSubscriptionJSONBody + +// CreateFalconEDRIntegrationJSONRequestBody defines body for CreateFalconEDRIntegration for application/json ContentType. +type CreateFalconEDRIntegrationJSONRequestBody = EDRFalconRequest + +// UpdateFalconEDRIntegrationJSONRequestBody defines body for UpdateFalconEDRIntegration for application/json ContentType. +type UpdateFalconEDRIntegrationJSONRequestBody = EDRFalconRequest + +// CreateHuntressEDRIntegrationJSONRequestBody defines body for CreateHuntressEDRIntegration for application/json ContentType. +type CreateHuntressEDRIntegrationJSONRequestBody = EDRHuntressRequest + +// UpdateHuntressEDRIntegrationJSONRequestBody defines body for UpdateHuntressEDRIntegration for application/json ContentType. +type UpdateHuntressEDRIntegrationJSONRequestBody = EDRHuntressRequest + +// CreateEDRIntegrationJSONRequestBody defines body for CreateEDRIntegration for application/json ContentType. +type CreateEDRIntegrationJSONRequestBody = EDRIntuneRequest + +// UpdateEDRIntegrationJSONRequestBody defines body for UpdateEDRIntegration for application/json ContentType. +type UpdateEDRIntegrationJSONRequestBody = EDRIntuneRequest + +// CreateSentinelOneEDRIntegrationJSONRequestBody defines body for CreateSentinelOneEDRIntegration for application/json ContentType. +type CreateSentinelOneEDRIntegrationJSONRequestBody = EDRSentinelOneRequest + +// UpdateSentinelOneEDRIntegrationJSONRequestBody defines body for UpdateSentinelOneEDRIntegration for application/json ContentType. +type UpdateSentinelOneEDRIntegrationJSONRequestBody = EDRSentinelOneRequest + +// PostApiIntegrationsMspTenantsJSONRequestBody defines body for PostApiIntegrationsMspTenants for application/json ContentType. +type PostApiIntegrationsMspTenantsJSONRequestBody = CreateTenantRequest + +// PutApiIntegrationsMspTenantsIdJSONRequestBody defines body for PutApiIntegrationsMspTenantsId for application/json ContentType. +type PutApiIntegrationsMspTenantsIdJSONRequestBody = UpdateTenantRequest + +// PutApiIntegrationsMspTenantsIdInviteJSONRequestBody defines body for PutApiIntegrationsMspTenantsIdInvite for application/json ContentType. +type PutApiIntegrationsMspTenantsIdInviteJSONRequestBody PutApiIntegrationsMspTenantsIdInviteJSONBody + +// PostApiIntegrationsMspTenantsIdSubscriptionJSONRequestBody defines body for PostApiIntegrationsMspTenantsIdSubscription for application/json ContentType. +type PostApiIntegrationsMspTenantsIdSubscriptionJSONRequestBody PostApiIntegrationsMspTenantsIdSubscriptionJSONBody + +// PostApiIntegrationsMspTenantsIdUnlinkJSONRequestBody defines body for PostApiIntegrationsMspTenantsIdUnlink for application/json ContentType. +type PostApiIntegrationsMspTenantsIdUnlinkJSONRequestBody PostApiIntegrationsMspTenantsIdUnlinkJSONBody + +// CreateSCIMIntegrationJSONRequestBody defines body for CreateSCIMIntegration for application/json ContentType. +type CreateSCIMIntegrationJSONRequestBody = CreateScimIntegrationRequest + +// UpdateSCIMIntegrationJSONRequestBody defines body for UpdateSCIMIntegration for application/json ContentType. +type UpdateSCIMIntegrationJSONRequestBody = UpdateScimIntegrationRequest + // PostApiNetworksJSONRequestBody defines body for PostApiNetworks for application/json ContentType. type PostApiNetworksJSONRequestBody = NetworkRequest