diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index d4342bf579b..0a8b2c26995 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -286,6 +286,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS if req.Settings.MetricsPushEnabled != nil { returnSettings.MetricsPushEnabled = *req.Settings.MetricsPushEnabled } + if req.Settings.AgentNetworkOnly != nil { + returnSettings.AgentNetworkOnly = *req.Settings.AgentNetworkOnly + } return returnSettings, nil } @@ -417,6 +420,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A AutoUpdateAlways: &settings.AutoUpdateAlways, Ipv6EnabledGroups: &settings.IPv6EnabledGroups, MetricsPushEnabled: &settings.MetricsPushEnabled, + AgentNetworkOnly: &settings.AgentNetworkOnly, EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled, LocalAuthDisabled: &settings.LocalAuthDisabled, LocalMfaEnabled: &settings.LocalMfaEnabled, diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index df89fde9ac8..d0bcbbc3bd2 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -130,6 +130,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateAlways: br(false), AutoUpdateVersion: sr(""), MetricsPushEnabled: br(false), + AgentNetworkOnly: br(false), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), LocalMfaEnabled: br(false), @@ -158,6 +159,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateAlways: br(false), AutoUpdateVersion: sr(""), MetricsPushEnabled: br(false), + AgentNetworkOnly: br(false), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), LocalMfaEnabled: br(false), @@ -186,6 +188,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateAlways: br(false), AutoUpdateVersion: sr("latest"), MetricsPushEnabled: br(false), + AgentNetworkOnly: br(false), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), LocalMfaEnabled: br(false), @@ -214,6 +217,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateAlways: br(false), AutoUpdateVersion: sr(""), MetricsPushEnabled: br(false), + AgentNetworkOnly: br(false), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), LocalMfaEnabled: br(false), @@ -242,6 +246,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateAlways: br(false), AutoUpdateVersion: sr(""), MetricsPushEnabled: br(false), + AgentNetworkOnly: br(false), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), LocalMfaEnabled: br(false), @@ -270,6 +275,65 @@ func TestAccounts_AccountsHandler(t *testing.T) { AutoUpdateAlways: br(false), AutoUpdateVersion: sr(""), MetricsPushEnabled: br(false), + AgentNetworkOnly: br(false), + EmbeddedIdpEnabled: br(false), + LocalAuthDisabled: br(false), + LocalMfaEnabled: br(false), + }, + expectedArray: false, + expectedID: accountID, + }, + { + name: "PutAccount OK enabling agent_network_only", + expectedBody: true, + requestType: http.MethodPut, + requestPath: "/api/accounts/" + accountID, + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true,\"agent_network_only\": true},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"), + expectedStatus: http.StatusOK, + expectedSettings: api.AccountSettings{ + PeerLoginExpiration: 15552000, + PeerLoginExpirationEnabled: true, + GroupsPropagationEnabled: br(false), + JwtGroupsClaimName: sr(""), + JwtGroupsEnabled: br(false), + JwtAllowGroups: &[]string{}, + RegularUsersViewBlocked: false, + RoutingPeerDnsResolutionEnabled: br(false), + LazyConnectionEnabled: br(false), + DnsDomain: sr(""), + AutoUpdateAlways: br(false), + AutoUpdateVersion: sr(""), + MetricsPushEnabled: br(false), + AgentNetworkOnly: br(true), + EmbeddedIdpEnabled: br(false), + LocalAuthDisabled: br(false), + LocalMfaEnabled: br(false), + }, + expectedArray: false, + expectedID: accountID, + }, + { + name: "PutAccount OK disabling agent_network_only again", + expectedBody: true, + requestType: http.MethodPut, + requestPath: "/api/accounts/" + accountID, + requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": true,\"agent_network_only\": false},\"onboarding\": {\"onboarding_flow_pending\": true,\"signup_form_pending\": true}}"), + expectedStatus: http.StatusOK, + expectedSettings: api.AccountSettings{ + PeerLoginExpiration: 15552000, + PeerLoginExpirationEnabled: true, + GroupsPropagationEnabled: br(false), + JwtGroupsClaimName: sr(""), + JwtGroupsEnabled: br(false), + JwtAllowGroups: &[]string{}, + RegularUsersViewBlocked: false, + RoutingPeerDnsResolutionEnabled: br(false), + LazyConnectionEnabled: br(false), + DnsDomain: sr(""), + AutoUpdateAlways: br(false), + AutoUpdateVersion: sr(""), + MetricsPushEnabled: br(false), + AgentNetworkOnly: br(false), EmbeddedIdpEnabled: br(false), LocalAuthDisabled: br(false), LocalMfaEnabled: br(false), diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 69efe65f139..c8ded4e4e28 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -1605,7 +1605,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc settings_jwt_groups_enabled, settings_jwt_groups_claim_name, settings_jwt_allow_groups, settings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range, settings_network_range_v6, settings_ipv6_enabled_groups, settings_lazy_connection_enabled, - settings_local_mfa_enabled, settings_metrics_push_enabled, + settings_local_mfa_enabled, settings_metrics_push_enabled, settings_agent_network_only, -- Embedded ExtraSettings settings_extra_peer_approval_enabled, settings_extra_user_approval_required, settings_extra_integrated_validator, settings_extra_integrated_validator_groups @@ -1629,6 +1629,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc sLazyConnectionEnabled sql.NullBool sLocalMFAEnabled sql.NullBool sMetricsPushEnabled sql.NullBool + sAgentNetworkOnly sql.NullBool sExtraPeerApprovalEnabled sql.NullBool sExtraUserApprovalRequired sql.NullBool sExtraIntegratedValidator sql.NullString @@ -1651,7 +1652,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc &sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups, &sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange, &sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled, - &sLocalMFAEnabled, &sMetricsPushEnabled, + &sLocalMFAEnabled, &sMetricsPushEnabled, &sAgentNetworkOnly, &sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired, &sExtraIntegratedValidator, &sExtraIntegratedValidatorGroups, ) @@ -1720,6 +1721,9 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc if sMetricsPushEnabled.Valid { account.Settings.MetricsPushEnabled = sMetricsPushEnabled.Bool } + if sAgentNetworkOnly.Valid { + account.Settings.AgentNetworkOnly = sAgentNetworkOnly.Bool + } if sJWTAllowGroups.Valid { _ = json.Unmarshal([]byte(sJWTAllowGroups.String), &account.Settings.JWTAllowGroups) } diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 92784af836f..faef8651e6e 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -1245,6 +1245,31 @@ func TestSqlite_CreateAndGetObjectInTransaction(t *testing.T) { assert.NoError(t, err) } +func TestSqlStore_SaveAccountPersistsAgentNetworkOnly(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + account, err := store.GetAccount(context.Background(), accountID) + require.NoError(t, err) + require.False(t, account.Settings.AgentNetworkOnly, "setting should default to false") + + account.Settings.AgentNetworkOnly = true + require.NoError(t, store.SaveAccount(context.Background(), account)) + + reloaded, err := store.GetAccount(context.Background(), accountID) + require.NoError(t, err) + require.True(t, reloaded.Settings.AgentNetworkOnly, "setting should survive a save/load round-trip") + + reloaded.Settings.AgentNetworkOnly = false + require.NoError(t, store.SaveAccount(context.Background(), reloaded)) + + disabled, err := store.GetAccount(context.Background(), accountID) + require.NoError(t, err) + require.False(t, disabled.Settings.AgentNetworkOnly, "disabling should persist") +} + func TestSqlStore_GetAccountUsers(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) t.Cleanup(cleanup) diff --git a/management/server/types/settings.go b/management/server/types/settings.go index d17d0ef2b6e..7c24f944b6b 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -76,6 +76,10 @@ type Settings struct { // MetricsPushEnabled globally enables or disables client metrics push for the account MetricsPushEnabled bool `gorm:"default:false"` + // AgentNetworkOnly limits the dashboard to the Agent Network surface for this account. + // Set for accounts created via netbird.ai signups; users can disable it later. + AgentNetworkOnly bool `gorm:"default:false"` + // EmbeddedIdpEnabled indicates if the embedded identity provider is enabled. // This is a runtime-only field, not stored in the database. EmbeddedIdpEnabled bool `gorm:"-"` @@ -114,6 +118,7 @@ func (s *Settings) Copy() *Settings { AutoUpdateAlways: s.AutoUpdateAlways, IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups), MetricsPushEnabled: s.MetricsPushEnabled, + AgentNetworkOnly: s.AgentNetworkOnly, EmbeddedIdpEnabled: s.EmbeddedIdpEnabled, LocalAuthDisabled: s.LocalAuthDisabled, LocalMfaEnabled: s.LocalMfaEnabled, diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index b38019a73fb..2305daaacd6 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -375,6 +375,10 @@ components: description: Enables or disables client metrics push for all peers in the account type: boolean example: false + agent_network_only: + description: Limits the dashboard to the Agent Network surface for this account. Set for accounts created via netbird.ai signups and can be disabled later. + type: boolean + example: false embedded_idp_enabled: description: Indicates whether the embedded identity provider (Dex) is enabled for this account. This is a read-only field. type: boolean diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 7d68a105295..6356aca18f7 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -1647,6 +1647,9 @@ type AccountRequest struct { // AccountSettings defines model for AccountSettings. type AccountSettings struct { + // AgentNetworkOnly Limits the dashboard to the Agent Network surface for this account. Set for accounts created via netbird.ai signups and can be disabled later. + AgentNetworkOnly *bool `json:"agent_network_only,omitempty"` + // AutoUpdateAlways When true, updates are installed automatically in the background. When false, updates require user interaction from the UI. AutoUpdateAlways *bool `json:"auto_update_always,omitempty"`