From 00003814f3561692b49569da771b9160075ae5d1 Mon Sep 17 00:00:00 2001 From: dmitri-netbird Date: Fri, 4 Sep 2026 15:29:51 +0200 Subject: [PATCH 1/2] [management] update network_router_test to verify empty and nil peer_groups (#7425) * update network_router_test to verify empty and nil peer_groups Signed-off-by: Dmitri Dolguikh * fix an issue with deserialization of nil json array in user.go Signed-off-by: Dmitri Dolguikh * order zones by id Signed-off-by: Dmitri Dolguikh --------- Signed-off-by: Dmitri Dolguikh --- .../network_map_db/network_router_test.go | 12 ++ .../management/network_map_db/user_test.go | 22 +++- .../network_router_store_test.go | 120 ------------------ .../internals/network_map_db/pgsql/dns.go | 1 + .../internals/network_map_db/sqlite/user.go | 11 +- 5 files changed, 36 insertions(+), 130 deletions(-) delete mode 100644 management/internals/network_map_db/network_router_store_test.go diff --git a/integration_tests/management/network_map_db/network_router_test.go b/integration_tests/management/network_map_db/network_router_test.go index fa7ea2a04..2baf146a7 100644 --- a/integration_tests/management/network_map_db/network_router_test.go +++ b/integration_tests/management/network_map_db/network_router_test.go @@ -19,6 +19,14 @@ func TestGetNetworkRouters(t *testing.T) { execQuery(t, ctx, `insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups) VALUES('test-nr-id-2','account-1','public-id-2','','network-id-2',TRUE,333,TRUE,'["group-two-resources-id","group-no-resources-id"]')`) + // empty peer_groups + execQuery(t, ctx, + `insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups) + VALUES('test-nr-id-3','account-1','public-id-3','peer-id-3','network-id-3',TRUE,999,TRUE,'[]')`) + // nil peer_groups + execQuery(t, ctx, + `insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups) + VALUES('test-nr-id-4','account-1','public-id-4','peer-id-4','network-id-4',TRUE,999,TRUE,null)`) routers, err := conn(t, ctx).GetNetworkRouters(ctx, "account-1") assert.NoError(t, err) @@ -30,4 +38,8 @@ func TestGetNetworkRouters(t *testing.T) { map[string]*nmdata.NetworkRouter{ "peer-id-2": {PublicID: "public-id-2", Masquerade: true, Metric: 333, Enabled: true, PeerGroups: []string{"group-two-resources-id", "group-no-resources-id"}}, "peer-id-3": {PublicID: "public-id-2", Masquerade: true, Metric: 333, Enabled: true, PeerGroups: []string{"group-two-resources-id", "group-no-resources-id"}}}) + assert.Equal(t, routers["network-id-3"], + map[string]*nmdata.NetworkRouter{"peer-id-3": {PublicID: "public-id-3", Masquerade: true, Metric: 999, Enabled: true, PeerGroups: []string{}}}) + assert.Equal(t, routers["network-id-4"], + map[string]*nmdata.NetworkRouter{"peer-id-4": {PublicID: "public-id-4", Masquerade: true, Metric: 999, Enabled: true, PeerGroups: nil}}) } diff --git a/integration_tests/management/network_map_db/user_test.go b/integration_tests/management/network_map_db/user_test.go index 132f749e2..fce1833d3 100644 --- a/integration_tests/management/network_map_db/user_test.go +++ b/integration_tests/management/network_map_db/user_test.go @@ -21,6 +21,14 @@ func TestGetAllowedUsers(t *testing.T) { execQuery(t, ctx, `insert into users (id, name, account_id, auto_groups, blocked, is_service_user) VALUES('user-3','user-3','account-1','["group-two-resources-id"]',false,false)`) + // empty auto_groups; shouldn't error out + execQuery(t, ctx, + `insert into users (id, name, account_id, auto_groups, blocked, is_service_user) + VALUES('user-31','user-31','account-1','[]',false,false)`) + // null auto_groups; shouldn't error out + execQuery(t, ctx, + `insert into users (id, name, account_id, auto_groups, blocked, is_service_user) + VALUES('user-32','user-32','account-1',null,false,false)`) // shouldn't be included as it's blocked execQuery(t, ctx, `insert into users (id, name, account_id, auto_groups, blocked, is_service_user) @@ -43,15 +51,17 @@ func TestGetAllowedUsers(t *testing.T) { assert.NoError(t, err) assert.Equal(t, userIdx, map[string]struct{}{ - "user-1": {}, - "user-2": {}, - "user-3": {}, + "user-1": {}, + "user-2": {}, + "user-3": {}, + "user-31": {}, + "user-32": {}, }) assert.Equal(t, groupIdToUserIds, map[string][]string{ "group-one-resource-id": {"user-1", "user-2"}, "group-two-resources-id": {"user-2", "user-3"}, - "all-group-1": {"user-1", "user-2", "user-3"}, - "all-group-2": {"user-1", "user-2", "user-3"}, - "all-group-3": {"user-1", "user-2", "user-3"}, + "all-group-1": {"user-1", "user-2", "user-3", "user-31", "user-32"}, + "all-group-2": {"user-1", "user-2", "user-3", "user-31", "user-32"}, + "all-group-3": {"user-1", "user-2", "user-3", "user-31", "user-32"}, }) } diff --git a/management/internals/network_map_db/network_router_store_test.go b/management/internals/network_map_db/network_router_store_test.go deleted file mode 100644 index d44aa1ebc..000000000 --- a/management/internals/network_map_db/network_router_store_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package networkmapdb_test - -import ( - "context" - "net/netip" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db" - networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql" - networkmap_sqlite "github.com/netbirdio/netbird/management/internals/network_map_db/sqlite" - routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" - nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/store" - "github.com/netbirdio/netbird/management/server/testutil" - "github.com/netbirdio/netbird/management/server/types" -) - -// newEngineStores opens both stores on the selected engine's database. -func newEngineStores(t *testing.T) (store.Store, networkmapdb.NetworkMapDBStore) { - t.Helper() - ctx := context.Background() - - switch engine := types.Engine(os.Getenv("NETBIRD_STORE_ENGINE")); engine { - case types.PostgresStoreEngine: - cleanup, dsn, err := testutil.CreatePostgresTestContainer() - require.NoError(t, err, "start postgres test container") - t.Cleanup(cleanup) - - accountStore, err := store.NewPostgresqlStore(ctx, dsn, nil, false) - require.NoError(t, err, "connect account store") - t.Cleanup(func() { _ = accountStore.Close(ctx) }) - - nmStore, err := networkmap_pgsql.NewPostgresqlStore(ctx, dsn) - require.NoError(t, err, "connect networkmap store") - t.Cleanup(func() { nmStore.Pool.Close() }) - return accountStore, nmStore - case types.SqliteStoreEngine, "": - dataDir := t.TempDir() - accountStore, err := store.NewSqliteStore(ctx, dataDir, nil, false) - require.NoError(t, err, "open account store") - t.Cleanup(func() { _ = accountStore.Close(ctx) }) - - nmStore, err := networkmap_sqlite.NewSqliteStore("store.db", dataDir) - require.NoError(t, err, "open networkmap store") - t.Cleanup(func() { _ = nmStore.Db.Close() }) - return accountStore, nmStore - default: - t.Skipf("networkmap store does not support engine %q", engine) - return nil, nil - } -} - -// Peer-based routers must survive the network-map read on every engine. -func TestGetNetworkRouters_ServesPeerBasedRouters(t *testing.T) { - ctx := context.Background() - accountStore, nmStore := newEngineStores(t) - - const ( - accountID = "acc-nmap-routers" - groupID = "grp-router-members" - memberID = "peer-member" - ) - - // Postgres enforces the groups-to-accounts FK that SQLite ignores. - require.NoError(t, accountStore.SaveAccount(ctx, &types.Account{ - Id: accountID, - Peers: map[string]*nbpeer.Peer{ - memberID: { - ID: memberID, - AccountID: accountID, - Key: memberID + "-key", - IP: netip.MustParseAddr("100.64.0.10"), - Status: &nbpeer.PeerStatus{}, - }, - }, - Groups: map[string]*types.Group{ - groupID: { - ID: groupID, - AccountID: accountID, - Name: "router members", - Issued: types.GroupIssuedAPI, - Peers: []string{memberID}, - }, - }, - }), "seed the account the routers belong to") - - routers := []*routerTypes.NetworkRouter{ - {ID: "router-peer-nil", AccountID: accountID, NetworkID: "net-peer-nil", PublicID: "pub-peer-nil", Peer: "peer-direct-nil", Enabled: true, Metric: 9999}, - {ID: "router-peer-empty", AccountID: accountID, NetworkID: "net-peer-empty", PublicID: "pub-peer-empty", Peer: "peer-direct-empty", PeerGroups: []string{}, Enabled: true, Metric: 9999}, - {ID: "router-group", AccountID: accountID, NetworkID: "net-group", PublicID: "pub-group", PeerGroups: []string{groupID}, Enabled: true, Metric: 9999}, - } - for _, router := range routers { - require.NoError(t, accountStore.CreateNetworkRouter(ctx, router)) - } - - tx, err := nmStore.BeginTx(ctx) - require.NoError(t, err, "begin networkmap read transaction") - t.Cleanup(func() { _ = tx.RollbackTx(ctx) }) - - got, err := tx.GetNetworkRouters(ctx, accountID) - require.NoError(t, err, "read network routers") - - assert.Contains(t, got, "net-peer-nil", - "a router referencing an individual peer (peer_groups stored as NULL) must reach the network map") - assert.Contains(t, got["net-peer-nil"], "peer-direct-nil", - "the individual-peer router must be keyed by its peer") - - assert.Contains(t, got, "net-peer-empty", - "a router referencing an individual peer (peer_groups stored as '[]') must reach the network map") - assert.Contains(t, got["net-peer-empty"], "peer-direct-empty", - "the individual-peer router must be keyed by its peer") - - assert.Contains(t, got, "net-group", "a group router must reach the network map") - assert.Contains(t, got["net-group"], memberID, - "the group router must fan out to the group's member peers") -} diff --git a/management/internals/network_map_db/pgsql/dns.go b/management/internals/network_map_db/pgsql/dns.go index b22b43903..46ef0ddfa 100644 --- a/management/internals/network_map_db/pgsql/dns.go +++ b/management/internals/network_map_db/pgsql/dns.go @@ -15,6 +15,7 @@ const ( from zones left join records as r on r.zone_id = zones.id where zones.account_id=$1 and zones.enabled + order by zones.id ` ) diff --git a/management/internals/network_map_db/sqlite/user.go b/management/internals/network_map_db/sqlite/user.go index 0bdda372e..23c46bf76 100644 --- a/management/internals/network_map_db/sqlite/user.go +++ b/management/internals/network_map_db/sqlite/user.go @@ -42,17 +42,20 @@ func (sc *SqliteStoreConn) GetAllowedUsers(ctx context.Context, accountId string userIdIdx := make(map[string]struct{}) groupIdToUserIds := make(map[string][]string) for _, user := range users { + for _, allgid := range allGroupIds { + groupIdToUserIds[allgid] = append(groupIdToUserIds[allgid], user.ID) + } + userIdIdx[user.ID] = struct{}{} autogroups := make([]string, 0) + if user.AutoGroups == nil { + continue + } if err := json.Unmarshal(user.AutoGroups, &autogroups); err != nil { return nil, nil, err } - userIdIdx[user.ID] = struct{}{} for _, groupId := range autogroups { groupIdToUserIds[groupId] = append(groupIdToUserIds[groupId], user.ID) } - for _, allgid := range allGroupIds { - groupIdToUserIds[allgid] = append(groupIdToUserIds[allgid], user.ID) - } } return userIdIdx, groupIdToUserIds, nil From 76ea72237f3346ff157aa8374b9fa800d6498976 Mon Sep 17 00:00:00 2001 From: Brad Ison Date: Fri, 4 Sep 2026 17:11:27 +0200 Subject: [PATCH 2/2] [management] Add Agent Network managed proxy to the API spec (#7433) Defines the cloud-side managed gateway provisioning surface (POST/GET /api/integrations/agent-network/managed-proxy) and its response objects so clients consume generated types instead of hand-written ones. POST is idempotent: 202 when the call starts (or restarts) provisioning, 200 when a deployment already exists; 409 names an already-assigned endpoint the managed flow does not own and 503 signals temporarily exhausted endpoint allocation. --- client/proto/generate.sh | 2 +- encryption/testprotos/generate.sh | 4 +- flow/proto/generate.sh | 2 +- shared/management/http/api/generate.sh | 2 +- shared/management/http/api/openapi.yml | 99 +++++++++++++++++++++++++ shared/management/http/api/types.gen.go | 48 ++++++++++++ shared/management/proto/generate.sh | 2 +- shared/signal/proto/generate.sh | 2 +- 8 files changed, 154 insertions(+), 7 deletions(-) diff --git a/client/proto/generate.sh b/client/proto/generate.sh index cea8ae912..d73367d12 100755 --- a/client/proto/generate.sh +++ b/client/proto/generate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e if ! which realpath >/dev/null 2>&1; then diff --git a/encryption/testprotos/generate.sh b/encryption/testprotos/generate.sh index 0ce6ebdea..ffbc481d6 100755 --- a/encryption/testprotos/generate.sh +++ b/encryption/testprotos/generate.sh @@ -1,2 +1,2 @@ -#!/bin/bash -protoc -I testprotos/ testprotos/testproto.proto --go_out=. \ No newline at end of file +#!/usr/bin/env bash +protoc -I testprotos/ testprotos/testproto.proto --go_out=. diff --git a/flow/proto/generate.sh b/flow/proto/generate.sh index 6bbf78e61..a031245fd 100755 --- a/flow/proto/generate.sh +++ b/flow/proto/generate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e if ! which realpath > /dev/null 2>&1 diff --git a/shared/management/http/api/generate.sh b/shared/management/http/api/generate.sh index ba29a6905..8f563e99a 100755 --- a/shared/management/http/api/generate.sh +++ b/shared/management/http/api/generate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e if ! which realpath > /dev/null 2>&1 diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index a0549b077..5ad682c6b 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -6443,6 +6443,44 @@ components: - enable_prompt_collection - redact_pii - access_log_retention_days + AgentNetworkManagedProxy: + type: object + description: A NetBird-managed Agent Network gateway deployment. + properties: + id: + type: string + description: Managed proxy deployment ID. + example: "d1m3kebd9pcs0c1pnu7g" + state: + type: string + description: Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure. + enum: [ "provisioning", "ready", "failed" ] + example: "ready" + endpoint: + type: string + description: The account's gateway hostname. + example: "brave-otter.gateway.netbird.io" + region: + type: string + description: Region of the cluster hosting the deployment. + example: "us-east" + message: + type: string + description: Failure detail reported by the rollout. Only set when state is `failed`. + required: + - id + - state + - endpoint + AgentNetworkManagedProxyConflict: + type: object + description: Conflict body returned when the account already has an Agent Network endpoint that managed provisioning does not own, naming that endpoint. + properties: + endpoint: + type: string + description: The Agent Network endpoint already assigned to the account. + example: "llm.example.com" + required: + - endpoint AgentNetworkBudgetRule: type: object description: Account-level budget rule. A limit-only rule bound to groups and/or users that applies across all policies as a min-wins ceiling. Empty targets means it applies to every caller. @@ -13531,6 +13569,67 @@ paths: "$ref": "#/components/responses/not_found" '500': "$ref": "#/components/responses/internal_error" + /api/integrations/agent-network/managed-proxy: + post: + summary: Provision a managed Agent Network gateway + description: Starts provisioning of a NetBird-managed Agent Network gateway for the account, allocating its endpoint under the managed zone on the first call. Idempotent — answers 202 when this call started (or, after a failure, restarted) provisioning and 200 when a deployment already exists, reporting current state either way. Returns 409 when the account already has an Agent Network endpoint that managed provisioning does not own, and 503 when endpoint allocation is temporarily exhausted. + tags: [ Agent Network ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: A managed gateway deployment already exists; reports its current state. + content: + application/json: + schema: + $ref: '#/components/schemas/AgentNetworkManagedProxy' + '202': + description: Provisioning started, or restarted after a reported failure + content: + application/json: + schema: + $ref: '#/components/schemas/AgentNetworkManagedProxy' + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '409': + description: The account already has an Agent Network endpoint not owned by managed provisioning + content: + application/json: + schema: + $ref: '#/components/schemas/AgentNetworkManagedProxyConflict' + '500': + "$ref": "#/components/responses/internal_error" + '503': + description: Endpoint allocation is temporarily exhausted; retry later + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + summary: Retrieve managed Agent Network gateway status + description: Reports the account's managed gateway deployment and its derived state. Returns 404 when the account has no managed deployment. + tags: [ Agent Network ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: The account's managed gateway deployment + content: + application/json: + schema: + $ref: '#/components/schemas/AgentNetworkManagedProxy' + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '500': + "$ref": "#/components/responses/internal_error" /api/agent-network/access-logs: get: summary: List Agent Network access logs diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index d6bebf134..b5a7a80ac 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -77,6 +77,27 @@ func (e AgentNetworkConsumptionDimensionKind) Valid() bool { } } +// Defines values for AgentNetworkManagedProxyState. +const ( + AgentNetworkManagedProxyStateFailed AgentNetworkManagedProxyState = "failed" + AgentNetworkManagedProxyStateProvisioning AgentNetworkManagedProxyState = "provisioning" + AgentNetworkManagedProxyStateReady AgentNetworkManagedProxyState = "ready" +) + +// Valid indicates whether the value is a known member of the AgentNetworkManagedProxyState enum. +func (e AgentNetworkManagedProxyState) Valid() bool { + switch e { + case AgentNetworkManagedProxyStateFailed: + return true + case AgentNetworkManagedProxyStateProvisioning: + return true + case AgentNetworkManagedProxyStateReady: + return true + default: + return false + } +} + // Defines values for CreateAzureIntegrationRequestHost. const ( CreateAzureIntegrationRequestHostMicrosoftCom CreateAzureIntegrationRequestHost = "microsoft.com" @@ -2224,6 +2245,33 @@ type AgentNetworkGuardrailRequest struct { Name string `json:"name"` } +// AgentNetworkManagedProxy A NetBird-managed Agent Network gateway deployment. +type AgentNetworkManagedProxy struct { + // Endpoint The account's gateway hostname. + Endpoint string `json:"endpoint"` + + // Id Managed proxy deployment ID. + Id string `json:"id"` + + // Message Failure detail reported by the rollout. Only set when state is `failed`. + Message *string `json:"message,omitempty"` + + // Region Region of the cluster hosting the deployment. + Region *string `json:"region,omitempty"` + + // State Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure. + State AgentNetworkManagedProxyState `json:"state"` +} + +// AgentNetworkManagedProxyState Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure. +type AgentNetworkManagedProxyState string + +// AgentNetworkManagedProxyConflict Conflict body returned when the account already has an Agent Network endpoint that managed provisioning does not own, naming that endpoint. +type AgentNetworkManagedProxyConflict struct { + // Endpoint The Agent Network endpoint already assigned to the account. + Endpoint string `json:"endpoint"` +} + // AgentNetworkModelDiscoveryRequest defines model for AgentNetworkModelDiscoveryRequest. type AgentNetworkModelDiscoveryRequest struct { // ApiKey Credential to query the vendor with, for a provider that has not been saved yet. Mutually exclusive with provider_id. diff --git a/shared/management/proto/generate.sh b/shared/management/proto/generate.sh index 7cb0f75a5..2915b7f0c 100755 --- a/shared/management/proto/generate.sh +++ b/shared/management/proto/generate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e if ! which realpath > /dev/null 2>&1 diff --git a/shared/signal/proto/generate.sh b/shared/signal/proto/generate.sh index 720a5ff66..718eae152 100755 --- a/shared/signal/proto/generate.sh +++ b/shared/signal/proto/generate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e if ! which realpath > /dev/null 2>&1