diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 2e499dc74..cf030f51e 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -5494,3 +5494,61 @@ func (s *SqlStore) CleanupStaleProxies(ctx context.Context, inactivityDuration t return nil } + +// GetRoutingPeerNetworks returns the distinct network names where the peer is assigned as a routing peer +// in an enabled network router, either directly or via peer groups. +func (s *SqlStore) GetRoutingPeerNetworks(_ context.Context, accountID, peerID string) ([]string, error) { + var routers []*routerTypes.NetworkRouter + if err := s.db.Select("peer, peer_groups, network_id").Where("account_id = ? AND enabled = true", accountID).Find(&routers).Error; err != nil { + return nil, status.Errorf(status.Internal, "failed to get enabled routers: %v", err) + } + + if len(routers) == 0 { + return nil, nil + } + + var groupPeers []types.GroupPeer + if err := s.db.Select("group_id").Where("account_id = ? AND peer_id = ?", accountID, peerID).Find(&groupPeers).Error; err != nil { + return nil, status.Errorf(status.Internal, "failed to get peer group memberships: %v", err) + } + + groupSet := make(map[string]struct{}, len(groupPeers)) + for _, gp := range groupPeers { + groupSet[gp.GroupID] = struct{}{} + } + + networkIDs := make(map[string]struct{}) + for _, r := range routers { + if r.Peer == peerID { + networkIDs[r.NetworkID] = struct{}{} + } else if r.Peer == "" { + for _, pg := range r.PeerGroups { + if _, ok := groupSet[pg]; ok { + networkIDs[r.NetworkID] = struct{}{} + break + } + } + } + } + + if len(networkIDs) == 0 { + return nil, nil + } + + ids := make([]string, 0, len(networkIDs)) + for id := range networkIDs { + ids = append(ids, id) + } + + var networks []*networkTypes.Network + if err := s.db.Select("name").Where("account_id = ? AND id IN ?", accountID, ids).Find(&networks).Error; err != nil { + return nil, status.Errorf(status.Internal, "failed to get networks: %v", err) + } + + names := make([]string, 0, len(networks)) + for _, n := range networks { + names = append(names, n.Name) + } + + return names, nil +} diff --git a/management/server/store/store.go b/management/server/store/store.go index 816dff4fa..d00dcde38 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -290,6 +290,8 @@ type Store interface { CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error) + + GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error) } const ( diff --git a/management/server/store/store_mock.go b/management/server/store/store_mock.go index d779a7bcd..235405861 100644 --- a/management/server/store/store_mock.go +++ b/management/server/store/store_mock.go @@ -2333,6 +2333,21 @@ func (mr *MockStoreMockRecorder) IncrementSetupKeyUsage(ctx, setupKeyID interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementSetupKeyUsage", reflect.TypeOf((*MockStore)(nil).IncrementSetupKeyUsage), ctx, setupKeyID) } +// GetRoutingPeerNetworks mocks base method. +func (m *MockStore) GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRoutingPeerNetworks", ctx, accountID, peerID) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRoutingPeerNetworks indicates an expected call of GetRoutingPeerNetworks. +func (mr *MockStoreMockRecorder) GetRoutingPeerNetworks(ctx, accountID, peerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoutingPeerNetworks", reflect.TypeOf((*MockStore)(nil).GetRoutingPeerNetworks), ctx, accountID, peerID) +} + // IsPrimaryAccount mocks base method. func (m *MockStore) IsPrimaryAccount(ctx context.Context, accountID string) (bool, string, error) { m.ctrl.T.Helper() diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 6b766731c..d81c371db 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -89,6 +89,10 @@ tags: - name: Event Streaming Integrations description: Manage event streaming integrations. x-cloud-only: true + - name: Notifications + description: Manage notification channels for account event alerts. + x-cloud-only: true + components: schemas: @@ -4385,6 +4389,123 @@ components: type: string description: The newly generated SCIM API token example: "nbs_F3f0d..." + NotificationChannelType: + type: string + description: The type of notification channel. + enum: + - email + - webhook + example: "email" + NotificationEventType: + type: string + description: | + An activity event type code. See `GET /api/integrations/notifications/types` for the full list + of supported event types and their human-readable descriptions. + example: "user.join" + EmailTarget: + type: object + description: Target configuration for email notification channels. + properties: + emails: + type: array + description: List of email addresses to send notifications to. + minItems: 1 + items: + type: string + format: email + example: [ "admin@example.com", "ops@example.com" ] + required: + - emails + WebhookTarget: + type: object + description: Target configuration for webhook notification channels. + properties: + url: + type: string + format: uri + description: The webhook endpoint URL to send notifications to. + example: "https://hooks.example.com/netbird" + headers: + type: object + additionalProperties: + type: string + description: | + Custom HTTP headers sent with each webhook request. + Values are write-only; in GET responses all values are masked. + example: + Authorization: "Bearer token" + X-Webhook-Secret: "secret" + required: + - url + NotificationChannelRequest: + type: object + description: Request body for creating or updating a notification channel. + properties: + type: + $ref: '#/components/schemas/NotificationChannelType' + target: + description: | + Channel-specific target configuration. The shape depends on the `type` field: + - `email`: requires an `EmailTarget` object + - `webhook`: requires a `WebhookTarget` object + oneOf: + - $ref: '#/components/schemas/EmailTarget' + - $ref: '#/components/schemas/WebhookTarget' + event_types: + type: array + description: List of activity event type codes this channel subscribes to. + items: + $ref: '#/components/schemas/NotificationEventType' + example: [ "user.join", "peer.user.add", "peer.login.expire" ] + enabled: + type: boolean + description: Whether this notification channel is active. + example: true + required: + - type + - event_types + - enabled + NotificationChannelResponse: + type: object + description: A notification channel configuration. + properties: + id: + type: string + description: Unique identifier of the notification channel. + readOnly: true + example: "ch8i4ug6lnn4g9hqv7m0" + type: + $ref: '#/components/schemas/NotificationChannelType' + target: + description: | + Channel-specific target configuration. The shape depends on the `type` field: + - `email`: an `EmailTarget` object + - `webhook`: a `WebhookTarget` object + oneOf: + - $ref: '#/components/schemas/EmailTarget' + - $ref: '#/components/schemas/WebhookTarget' + event_types: + type: array + description: List of activity event type codes this channel subscribes to. + items: + $ref: '#/components/schemas/NotificationEventType' + example: [ "user.join", "peer.user.add", "peer.login.expire" ] + enabled: + type: boolean + description: Whether this notification channel is active. + example: true + required: + - id + - type + - event_types + - enabled + NotificationTypeEntry: + type: object + description: A map of event type codes to their human-readable descriptions. + additionalProperties: + type: string + example: + user.join: "User joined" BypassResponse: type: object description: Response for bypassed peer operations. @@ -10062,3 +10183,172 @@ paths: "$ref": "#/components/responses/not_found" '500': "$ref": "#/components/responses/internal_error" + /api/integrations/notifications/types: + get: + tags: + - Notifications + summary: List Notification Event Types + description: | + Returns a map of all supported activity event type codes to their + human-readable descriptions. Use these codes when configuring + `event_types` on notification channels. + operationId: listNotificationEventTypes + responses: + '200': + description: A map of event type codes to descriptions. + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationTypeEntry' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" + /api/integrations/notifications/channels: + get: + tags: + - Notifications + summary: List Notification Channels + description: Retrieves all notification channels configured for the authenticated account. + operationId: listNotificationChannels + responses: + '200': + description: A list of notification channels. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NotificationChannelResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" + post: + tags: + - Notifications + summary: Create Notification Channel + description: | + Creates a new notification channel for the authenticated account. + Supported channel types are `email` and `webhook`. + operationId: createNotificationChannel + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationChannelRequest' + responses: + '200': + description: Notification channel created successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationChannelResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" + /api/integrations/notifications/channels/{channelId}: + parameters: + - name: channelId + in: path + required: true + description: The unique identifier of the notification channel. + schema: + type: string + example: "ch8i4ug6lnn4g9hqv7m0" + get: + tags: + - Notifications + summary: Get Notification Channel + description: Retrieves a specific notification channel by its ID. + operationId: getNotificationChannel + responses: + '200': + description: Successfully retrieved the notification channel. + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationChannelResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" + put: + tags: + - Notifications + summary: Update Notification Channel + description: Updates an existing notification channel. + operationId: updateNotificationChannel + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationChannelRequest' + responses: + '200': + description: Notification channel updated successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationChannelResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" + delete: + tags: + - Notifications + summary: Delete Notification Channel + description: Deletes a notification channel by its ID. + operationId: deleteNotificationChannel + responses: + '200': + description: Notification channel deleted successfully. + content: + application/json: + schema: + type: object + example: { } + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 067cc4093..19d2706e1 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -9,6 +9,7 @@ import ( "time" "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" ) const ( @@ -664,6 +665,24 @@ func (e NetworkResourceType) Valid() bool { } } +// Defines values for NotificationChannelType. +const ( + NotificationChannelTypeEmail NotificationChannelType = "email" + NotificationChannelTypeWebhook NotificationChannelType = "webhook" +) + +// Valid indicates whether the value is a known member of the NotificationChannelType enum. +func (e NotificationChannelType) Valid() bool { + switch e { + case NotificationChannelTypeEmail: + return true + case NotificationChannelTypeWebhook: + return true + default: + return false + } +} + // Defines values for PeerNetworkRangeCheckAction. const ( PeerNetworkRangeCheckActionAllow PeerNetworkRangeCheckAction = "allow" @@ -1893,6 +1912,12 @@ type EDRSentinelOneResponse struct { UpdatedAt time.Time `json:"updated_at"` } +// EmailTarget Target configuration for email notification channels. +type EmailTarget struct { + // Emails List of email addresses to send notifications to. + Emails []openapi_types.Email `json:"emails"` +} + // 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. @@ -2666,6 +2691,67 @@ type NetworkTrafficUser struct { Name string `json:"name"` } +// NotificationChannelRequest Request body for creating or updating a notification channel. +type NotificationChannelRequest struct { + // Enabled Whether this notification channel is active. + Enabled bool `json:"enabled"` + + // EventTypes List of activity event type codes this channel subscribes to. + EventTypes []NotificationEventType `json:"event_types"` + + // Target Channel-specific target configuration. The shape depends on the `type` field: + // - `email`: requires an `EmailTarget` object + // - `webhook`: requires a `WebhookTarget` object + Target *NotificationChannelRequest_Target `json:"target,omitempty"` + + // Type The type of notification channel. + Type NotificationChannelType `json:"type"` +} + +// NotificationChannelRequest_Target Channel-specific target configuration. The shape depends on the `type` field: +// - `email`: requires an `EmailTarget` object +// - `webhook`: requires a `WebhookTarget` object +type NotificationChannelRequest_Target struct { + union json.RawMessage +} + +// NotificationChannelResponse A notification channel configuration. +type NotificationChannelResponse struct { + // Enabled Whether this notification channel is active. + Enabled bool `json:"enabled"` + + // EventTypes List of activity event type codes this channel subscribes to. + EventTypes []NotificationEventType `json:"event_types"` + + // Id Unique identifier of the notification channel. + Id *string `json:"id,omitempty"` + + // Target Channel-specific target configuration. The shape depends on the `type` field: + // - `email`: an `EmailTarget` object + // - `webhook`: a `WebhookTarget` object + Target *NotificationChannelResponse_Target `json:"target,omitempty"` + + // Type The type of notification channel. + Type NotificationChannelType `json:"type"` +} + +// NotificationChannelResponse_Target Channel-specific target configuration. The shape depends on the `type` field: +// - `email`: an `EmailTarget` object +// - `webhook`: a `WebhookTarget` object +type NotificationChannelResponse_Target struct { + union json.RawMessage +} + +// NotificationChannelType The type of notification channel. +type NotificationChannelType string + +// NotificationEventType An activity event type code. See `GET /api/integrations/notifications/types` for the full list +// of supported event types and their human-readable descriptions. +type NotificationEventType = string + +// NotificationTypeEntry A map of event type codes to their human-readable descriptions. +type NotificationTypeEntry map[string]string + // OSVersionCheck Posture check for the version of operating system type OSVersionCheck struct { // Android Posture check for the version of operating system @@ -4211,6 +4297,16 @@ type UserRequest struct { Role string `json:"role"` } +// WebhookTarget Target configuration for webhook notification channels. +type WebhookTarget struct { + // Headers Custom HTTP headers sent with each webhook request. + // Values are write-only; in GET responses all values are masked. + Headers *map[string]string `json:"headers,omitempty"` + + // Url The webhook endpoint URL to send notifications to. + Url string `json:"url"` +} + // WorkloadRequest defines model for WorkloadRequest. type WorkloadRequest struct { union json.RawMessage @@ -4564,6 +4660,12 @@ type PostApiIntegrationsMspTenantsIdSubscriptionJSONRequestBody PostApiIntegrati // PostApiIntegrationsMspTenantsIdUnlinkJSONRequestBody defines body for PostApiIntegrationsMspTenantsIdUnlink for application/json ContentType. type PostApiIntegrationsMspTenantsIdUnlinkJSONRequestBody PostApiIntegrationsMspTenantsIdUnlinkJSONBody +// CreateNotificationChannelJSONRequestBody defines body for CreateNotificationChannel for application/json ContentType. +type CreateNotificationChannelJSONRequestBody = NotificationChannelRequest + +// UpdateNotificationChannelJSONRequestBody defines body for UpdateNotificationChannel for application/json ContentType. +type UpdateNotificationChannelJSONRequestBody = NotificationChannelRequest + // CreateSCIMIntegrationJSONRequestBody defines body for CreateSCIMIntegration for application/json ContentType. type CreateSCIMIntegrationJSONRequestBody = CreateScimIntegrationRequest @@ -4660,6 +4762,130 @@ type PutApiUsersUserIdPasswordJSONRequestBody = PasswordChangeRequest // PostApiUsersUserIdTokensJSONRequestBody defines body for PostApiUsersUserIdTokens for application/json ContentType. type PostApiUsersUserIdTokensJSONRequestBody = PersonalAccessTokenRequest +// AsEmailTarget returns the union data inside the NotificationChannelRequest_Target as a EmailTarget +func (t NotificationChannelRequest_Target) AsEmailTarget() (EmailTarget, error) { + var body EmailTarget + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromEmailTarget overwrites any union data inside the NotificationChannelRequest_Target as the provided EmailTarget +func (t *NotificationChannelRequest_Target) FromEmailTarget(v EmailTarget) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeEmailTarget performs a merge with any union data inside the NotificationChannelRequest_Target, using the provided EmailTarget +func (t *NotificationChannelRequest_Target) MergeEmailTarget(v EmailTarget) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsWebhookTarget returns the union data inside the NotificationChannelRequest_Target as a WebhookTarget +func (t NotificationChannelRequest_Target) AsWebhookTarget() (WebhookTarget, error) { + var body WebhookTarget + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromWebhookTarget overwrites any union data inside the NotificationChannelRequest_Target as the provided WebhookTarget +func (t *NotificationChannelRequest_Target) FromWebhookTarget(v WebhookTarget) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeWebhookTarget performs a merge with any union data inside the NotificationChannelRequest_Target, using the provided WebhookTarget +func (t *NotificationChannelRequest_Target) MergeWebhookTarget(v WebhookTarget) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t NotificationChannelRequest_Target) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *NotificationChannelRequest_Target) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// AsEmailTarget returns the union data inside the NotificationChannelResponse_Target as a EmailTarget +func (t NotificationChannelResponse_Target) AsEmailTarget() (EmailTarget, error) { + var body EmailTarget + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromEmailTarget overwrites any union data inside the NotificationChannelResponse_Target as the provided EmailTarget +func (t *NotificationChannelResponse_Target) FromEmailTarget(v EmailTarget) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeEmailTarget performs a merge with any union data inside the NotificationChannelResponse_Target, using the provided EmailTarget +func (t *NotificationChannelResponse_Target) MergeEmailTarget(v EmailTarget) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsWebhookTarget returns the union data inside the NotificationChannelResponse_Target as a WebhookTarget +func (t NotificationChannelResponse_Target) AsWebhookTarget() (WebhookTarget, error) { + var body WebhookTarget + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromWebhookTarget overwrites any union data inside the NotificationChannelResponse_Target as the provided WebhookTarget +func (t *NotificationChannelResponse_Target) FromWebhookTarget(v WebhookTarget) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeWebhookTarget performs a merge with any union data inside the NotificationChannelResponse_Target, using the provided WebhookTarget +func (t *NotificationChannelResponse_Target) MergeWebhookTarget(v WebhookTarget) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t NotificationChannelResponse_Target) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *NotificationChannelResponse_Target) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsBundleWorkloadRequest returns the union data inside the WorkloadRequest as a BundleWorkloadRequest func (t WorkloadRequest) AsBundleWorkloadRequest() (BundleWorkloadRequest, error) { var body BundleWorkloadRequest