diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 2884cfaf4..b9ea605d3 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/netip" - "os" "github.com/gorilla/mux" "github.com/rs/cors" @@ -63,10 +62,7 @@ import ( "github.com/netbirdio/netbird/management/server/telemetry" ) -const ( - apiPrefix = "/api" - setupCreatePATEnabledKey = "NB_SETUP_PAT_ENABLED" -) +const apiPrefix = "/api" // NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints. func NewAPIHandler(ctx context.Context, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, integratedValidator integrated_validator.IntegratedValidator, proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix, rateLimiter *middleware.APIRateLimiter) (http.Handler, error) { @@ -143,8 +139,7 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks zonesManager.RegisterEndpoints(router, zManager) recordsManager.RegisterEndpoints(router, rManager) idp.AddEndpoints(accountManager, router) - setupCreatePATEnabled := os.Getenv(setupCreatePATEnabledKey) == "true" - instance.AddEndpoints(instanceManager, accountManager, setupCreatePATEnabled, router) + instance.AddEndpoints(instanceManager, accountManager, router) instance.AddVersionEndpoint(instanceManager, router) if serviceManager != nil && reverseProxyDomainManager != nil { reverseproxymanager.RegisterEndpoints(serviceManager, *reverseProxyDomainManager, reverseProxyAccessLogsManager, permissionsManager, router) diff --git a/management/server/http/handlers/instance/instance_handler.go b/management/server/http/handlers/instance/instance_handler.go index 422162ae9..0537e7ad1 100644 --- a/management/server/http/handlers/instance/instance_handler.go +++ b/management/server/http/handlers/instance/instance_handler.go @@ -11,37 +11,20 @@ import ( nbinstance "github.com/netbirdio/netbird/management/server/instance" "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/shared/management/http/util" - "github.com/netbirdio/netbird/shared/management/status" -) - -// patMinExpireDays and patMaxExpireDays mirror the bounds enforced by -// DefaultAccountManager.CreatePAT in management/server/user.go. They are -// duplicated here so /api/setup can reject invalid input before it creates -// the embedded-IdP user. -const ( - patMinExpireDays = 1 - patMaxExpireDays = 365 ) // handler handles the instance setup HTTP endpoints type handler struct { - instanceManager nbinstance.Manager - setupManager *nbinstance.SetupService - createPATEnabled bool + instanceManager nbinstance.Manager + setupManager *nbinstance.SetupService } // AddEndpoints registers the instance setup endpoints. // These endpoints bypass authentication for initial setup. -// -// createPATEnabled toggles the setup-time Personal Access Token feature -// (NB_SETUP_PAT_ENABLED). When false, the create_pat / pat_expire_in request -// fields are silently ignored, matching the "env-gated, opt-in" pattern used -// by other optional features (e.g. rate limiting). -func AddEndpoints(instanceManager nbinstance.Manager, accountManager account.Manager, createPATEnabled bool, router *mux.Router) { +func AddEndpoints(instanceManager nbinstance.Manager, accountManager account.Manager, router *mux.Router) { h := &handler{ - instanceManager: instanceManager, - setupManager: nbinstance.NewSetupService(instanceManager, accountManager), - createPATEnabled: createPATEnabled, + instanceManager: instanceManager, + setupManager: nbinstance.NewSetupService(instanceManager, accountManager), } router.HandleFunc("/instance", h.getInstanceStatus).Methods("GET", "OPTIONS") @@ -83,21 +66,9 @@ func (h *handler) setup(w http.ResponseWriter, r *http.Request) { return } - wantPAT := h.createPATEnabled && req.CreatePat != nil && *req.CreatePat - if wantPAT { - if req.PatExpireIn == nil { - util.WriteError(ctx, status.Errorf(status.InvalidArgument, "pat_expire_in is required when create_pat is true"), w) - return - } - if *req.PatExpireIn < patMinExpireDays || *req.PatExpireIn > patMaxExpireDays { - util.WriteError(ctx, status.Errorf(status.InvalidArgument, "pat_expire_in must be between %d and %d", patMinExpireDays, patMaxExpireDays), w) - return - } - } - result, err := h.setupManager.SetupOwner(ctx, req.Email, req.Password, req.Name, nbinstance.SetupOptions{ - CreatePAT: wantPAT, - PATExpireInDays: expireInDays(req.PatExpireIn), + CreatePAT: req.CreatePat != nil && *req.CreatePat, + PATExpireInDays: req.PatExpireIn, }) if err != nil { util.WriteError(ctx, err, w) @@ -118,13 +89,6 @@ func (h *handler) setup(w http.ResponseWriter, r *http.Request) { util.WriteJSONObject(ctx, w, resp) } -func expireInDays(expireIn *int) int { - if expireIn == nil { - return 0 - } - return *expireIn -} - // getVersionInfo returns version information for NetBird components. // This endpoint requires authentication. func (h *handler) getVersionInfo(w http.ResponseWriter, r *http.Request) { diff --git a/management/server/http/handlers/instance/instance_handler_test.go b/management/server/http/handlers/instance/instance_handler_test.go index a2d3d190e..76e8f3a21 100644 --- a/management/server/http/handlers/instance/instance_handler_test.go +++ b/management/server/http/handlers/instance/instance_handler_test.go @@ -96,12 +96,12 @@ func (m *mockInstanceManager) GetVersionInfo(ctx context.Context) (*nbinstance.V var _ nbinstance.Manager = (*mockInstanceManager)(nil) func setupTestRouter(manager nbinstance.Manager) *mux.Router { - return setupTestRouterWithPAT(manager, nil, false) + return setupTestRouterWithPAT(manager, nil) } -func setupTestRouterWithPAT(manager nbinstance.Manager, accountManager account.Manager, createPATEnabled bool) *mux.Router { +func setupTestRouterWithPAT(manager nbinstance.Manager, accountManager account.Manager) *mux.Router { router := mux.NewRouter() - AddEndpoints(manager, accountManager, createPATEnabled, router) + AddEndpoints(manager, accountManager, router) return router } @@ -312,11 +312,13 @@ func TestSetup_ManagerError(t *testing.T) { } func TestSetup_PAT_FeatureDisabled_IgnoresCreatePAT(t *testing.T) { - manager := &mockInstanceManager{isSetupRequired: true} - // createPATEnabled=false: request fields must be silently ignored - router := setupTestRouterWithPAT(manager, &mock_server.MockAccountManager{}, false) + t.Setenv(nbinstance.SetupPATEnabledEnvKey, "false") - body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin", "create_pat": true, "pat_expire_in": 30}` + manager := &mockInstanceManager{isSetupRequired: true} + // NB_SETUP_PAT_ENABLED=false: request fields must be silently ignored + router := setupTestRouterWithPAT(manager, &mock_server.MockAccountManager{}) + + body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin", "create_pat": true}` req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewBufferString(body)) rec := httptest.NewRecorder() @@ -329,8 +331,10 @@ func TestSetup_PAT_FeatureDisabled_IgnoresCreatePAT(t *testing.T) { } func TestSetup_PAT_FlagOmitted_NoPAT(t *testing.T) { + t.Setenv(nbinstance.SetupPATEnabledEnvKey, "true") + manager := &mockInstanceManager{isSetupRequired: true} - router := setupTestRouterWithPAT(manager, &mock_server.MockAccountManager{}, true) + router := setupTestRouterWithPAT(manager, &mock_server.MockAccountManager{}) body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin"}` req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewBufferString(body)) @@ -344,7 +348,9 @@ func TestSetup_PAT_FlagOmitted_NoPAT(t *testing.T) { assert.Nil(t, response.PersonalAccessToken) } -func TestSetup_PAT_MissingExpireIn(t *testing.T) { +func TestSetup_PAT_MissingExpireIn_DefaultsToOneDay(t *testing.T) { + t.Setenv(nbinstance.SetupPATEnabledEnvKey, "true") + createCalled := false manager := &mockInstanceManager{ isSetupRequired: true, @@ -353,7 +359,21 @@ func TestSetup_PAT_MissingExpireIn(t *testing.T) { return &idp.UserData{ID: "u1", Email: email, Name: name}, nil }, } - router := setupTestRouterWithPAT(manager, &mock_server.MockAccountManager{}, true) + accountMgr := &mock_server.MockAccountManager{ + GetAccountIDByUserIdFunc: func(_ context.Context, userAuth auth.UserAuth) (string, error) { + assert.Equal(t, "u1", userAuth.UserId) + return "acc-1", nil + }, + CreatePATFunc: func(_ context.Context, accountID, initiator, target, name string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) { + assert.Equal(t, "acc-1", accountID) + assert.Equal(t, "u1", initiator) + assert.Equal(t, "u1", target) + assert.Equal(t, "setup-token", name) + assert.Equal(t, 1, expiresIn) + return &types.PersonalAccessTokenGenerated{PlainToken: "nbp_plain"}, nil + }, + } + router := setupTestRouterWithPAT(manager, accountMgr) body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin", "create_pat": true}` req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewBufferString(body)) @@ -361,13 +381,19 @@ func TestSetup_PAT_MissingExpireIn(t *testing.T) { router.ServeHTTP(rec, req) - assert.Equal(t, http.StatusUnprocessableEntity, rec.Code) - assert.False(t, createCalled, "CreateOwnerUser must not run when input is rejected") + assert.Equal(t, http.StatusOK, rec.Code) + assert.True(t, createCalled) + var response api.SetupResponse + require.NoError(t, json.NewDecoder(rec.Body).Decode(&response)) + require.NotNil(t, response.PersonalAccessToken) + assert.Equal(t, "nbp_plain", *response.PersonalAccessToken) } func TestSetup_PAT_ExpireOutOfRange(t *testing.T) { + t.Setenv(nbinstance.SetupPATEnabledEnvKey, "true") + manager := &mockInstanceManager{isSetupRequired: true} - router := setupTestRouterWithPAT(manager, &mock_server.MockAccountManager{}, true) + router := setupTestRouterWithPAT(manager, &mock_server.MockAccountManager{}) body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin", "create_pat": true, "pat_expire_in": 0}` req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewBufferString(body)) @@ -379,6 +405,8 @@ func TestSetup_PAT_ExpireOutOfRange(t *testing.T) { } func TestSetup_PAT_Success(t *testing.T) { + t.Setenv(nbinstance.SetupPATEnabledEnvKey, "true") + manager := &mockInstanceManager{ isSetupRequired: true, createOwnerUserFn: func(ctx context.Context, email, password, name string) (*idp.UserData, error) { @@ -406,7 +434,7 @@ func TestSetup_PAT_Success(t *testing.T) { }, } - router := setupTestRouterWithPAT(manager, accountMgr, true) + router := setupTestRouterWithPAT(manager, accountMgr) body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin", "create_pat": true, "pat_expire_in": 30}` req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewBufferString(body)) @@ -424,6 +452,8 @@ func TestSetup_PAT_Success(t *testing.T) { } func TestSetup_PAT_AccountCreationFails_Rollback(t *testing.T) { + t.Setenv(nbinstance.SetupPATEnabledEnvKey, "true") + rolledBackFor := "" manager := &mockInstanceManager{ isSetupRequired: true, @@ -441,7 +471,7 @@ func TestSetup_PAT_AccountCreationFails_Rollback(t *testing.T) { }, } - router := setupTestRouterWithPAT(manager, accountMgr, true) + router := setupTestRouterWithPAT(manager, accountMgr) body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin", "create_pat": true, "pat_expire_in": 30}` req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewBufferString(body)) @@ -454,6 +484,8 @@ func TestSetup_PAT_AccountCreationFails_Rollback(t *testing.T) { } func TestSetup_PAT_CreatePATFails_Rollback(t *testing.T) { + t.Setenv(nbinstance.SetupPATEnabledEnvKey, "true") + ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) account := &types.Account{Id: "acc-1"} @@ -483,7 +515,7 @@ func TestSetup_PAT_CreatePATFails_Rollback(t *testing.T) { }, } - router := setupTestRouterWithPAT(manager, accountMgr, true) + router := setupTestRouterWithPAT(manager, accountMgr) body := `{"email": "admin@example.com", "password": "securepassword123", "name": "Admin", "create_pat": true, "pat_expire_in": 30}` req := httptest.NewRequest(http.MethodPost, "/setup", bytes.NewBufferString(body)) diff --git a/management/server/instance/setup_service.go b/management/server/instance/setup_service.go index 3465292c0..7fec9592a 100644 --- a/management/server/instance/setup_service.go +++ b/management/server/instance/setup_service.go @@ -3,20 +3,40 @@ package instance import ( "context" "fmt" + "os" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/shared/auth" + "github.com/netbirdio/netbird/shared/management/status" ) -const setupPATTokenName = "setup-token" +const ( + setupPATTokenName = "setup-token" + + // SetupPATEnabledEnvKey enables setup-time Personal Access Token creation. + SetupPATEnabledEnvKey = "NB_SETUP_PAT_ENABLED" + + setupPATDefaultExpireDays = 1 + + // patMinExpireDays and patMaxExpireDays mirror the bounds enforced by + // DefaultAccountManager.CreatePAT in management/server/user.go. They are + // duplicated here so /api/setup can reject invalid input before it creates + // the embedded-IdP user. + patMinExpireDays = 1 + patMaxExpireDays = 365 +) // SetupOptions controls optional work performed during initial instance setup. type SetupOptions struct { - CreatePAT bool - PATExpireInDays int + // CreatePAT requests creation of a setup Personal Access Token. It is honored + // only when SetupPATEnabledEnvKey is set to "true". + CreatePAT bool + // PATExpireInDays defaults to 1 day when CreatePAT is requested and setup PAT + // creation is enabled. + PATExpireInDays *int } // SetupResult contains resources created during initial instance setup. @@ -31,6 +51,7 @@ type SetupResult struct { type SetupService struct { instanceManager Manager accountManager account.Manager + setupPATEnabled bool } // NewSetupService creates a setup use-case service. @@ -38,13 +59,43 @@ func NewSetupService(instanceManager Manager, accountManager account.Manager) *S return &SetupService{ instanceManager: instanceManager, accountManager: accountManager, + setupPATEnabled: os.Getenv(SetupPATEnabledEnvKey) == "true", } } -// SetupOwner creates the initial owner user and, optionally, provisions the -// account and a setup Personal Access Token. If account or PAT provisioning -// fails, created resources are rolled back so setup can be retried. +func normalizeSetupOptions(opts SetupOptions, setupPATEnabled bool) (SetupOptions, error) { + if !opts.CreatePAT { + return opts, nil + } + + if !setupPATEnabled { + opts.CreatePAT = false + opts.PATExpireInDays = nil + return opts, nil + } + + if opts.PATExpireInDays == nil { + defaultExpireInDays := setupPATDefaultExpireDays + opts.PATExpireInDays = &defaultExpireInDays + } + + if *opts.PATExpireInDays < patMinExpireDays || *opts.PATExpireInDays > patMaxExpireDays { + return opts, status.Errorf(status.InvalidArgument, "pat_expire_in must be between %d and %d", patMinExpireDays, patMaxExpireDays) + } + + return opts, nil +} + +// SetupOwner creates the initial owner user and, when requested and enabled by +// SetupPATEnabledEnvKey, provisions the account and a setup Personal Access +// Token. If account or PAT provisioning fails, created resources are rolled +// back so setup can be retried. func (m *SetupService) SetupOwner(ctx context.Context, email, password, name string, opts SetupOptions) (*SetupResult, error) { + opts, err := normalizeSetupOptions(opts, m.setupPATEnabled) + if err != nil { + return nil, err + } + userData, err := m.instanceManager.CreateOwnerUser(ctx, email, password, name) if err != nil { return nil, err @@ -74,7 +125,7 @@ func (m *SetupService) SetupOwner(ctx context.Context, email, password, name str return nil, err } - pat, err := m.accountManager.CreatePAT(ctx, accountID, userData.ID, userData.ID, setupPATTokenName, opts.PATExpireInDays) + pat, err := m.accountManager.CreatePAT(ctx, accountID, userData.ID, userData.ID, setupPATTokenName, *opts.PATExpireInDays) if err != nil { err = fmt.Errorf("create setup PAT: %w", err) m.rollbackSetup(ctx, userData.ID, "setup PAT provisioning failed", err, accountID) diff --git a/management/server/instance/setup_service_test.go b/management/server/instance/setup_service_test.go index 0f9d46c62..7cc374b95 100644 --- a/management/server/instance/setup_service_test.go +++ b/management/server/instance/setup_service_test.go @@ -46,7 +46,75 @@ func (m *setupInstanceManagerMock) GetVersionInfo(context.Context) (*VersionInfo var _ Manager = (*setupInstanceManagerMock)(nil) +func intPtr(v int) *int { + return &v +} + +func TestSetupOwner_PATFeatureDisabled_IgnoresCreatePAT(t *testing.T) { + t.Setenv(SetupPATEnabledEnvKey, "false") + + createCalls := 0 + setupManager := NewSetupService( + &setupInstanceManagerMock{ + createOwnerUserFn: func(_ context.Context, email, _, name string) (*idp.UserData, error) { + createCalls++ + return &idp.UserData{ID: "owner-id", Email: email, Name: name}, nil + }, + }, + &mock_server.MockAccountManager{}, + ) + + result, err := setupManager.SetupOwner(context.Background(), "admin@example.com", "securepassword123", "Admin", SetupOptions{ + CreatePAT: true, + }) + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "owner-id", result.User.ID) + assert.Empty(t, result.PATPlainToken) + assert.Equal(t, 1, createCalls) +} + +func TestSetupOwner_PATFeatureEnabled_MissingExpireDefaultsToOneDay(t *testing.T) { + t.Setenv(SetupPATEnabledEnvKey, "true") + + createCalled := false + setupManager := NewSetupService( + &setupInstanceManagerMock{ + createOwnerUserFn: func(_ context.Context, email, _, name string) (*idp.UserData, error) { + createCalled = true + return &idp.UserData{ID: "owner-id", Email: email, Name: name}, nil + }, + }, + &mock_server.MockAccountManager{ + GetAccountIDByUserIdFunc: func(_ context.Context, userAuth auth.UserAuth) (string, error) { + assert.Equal(t, "owner-id", userAuth.UserId) + return "acc-1", nil + }, + CreatePATFunc: func(_ context.Context, accountID, initiatorUserID, targetUserID, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) { + assert.Equal(t, "acc-1", accountID) + assert.Equal(t, "owner-id", initiatorUserID) + assert.Equal(t, "owner-id", targetUserID) + assert.Equal(t, setupPATTokenName, tokenName) + assert.Equal(t, setupPATDefaultExpireDays, expiresIn) + return &types.PersonalAccessTokenGenerated{PlainToken: "nbp_plain"}, nil + }, + }, + ) + + result, err := setupManager.SetupOwner(context.Background(), "admin@example.com", "securepassword123", "Admin", SetupOptions{ + CreatePAT: true, + }) + + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, createCalled) + assert.Equal(t, "nbp_plain", result.PATPlainToken) +} + func TestSetupOwner_CreatePATFails_RollsBackSetupAccountAndUser(t *testing.T) { + t.Setenv(SetupPATEnabledEnvKey, "true") + ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) account := &types.Account{Id: "acc-1"} @@ -83,7 +151,7 @@ func TestSetupOwner_CreatePATFails_RollsBackSetupAccountAndUser(t *testing.T) { result, err := setupManager.SetupOwner(context.Background(), "admin@example.com", "securepassword123", "Admin", SetupOptions{ CreatePAT: true, - PATExpireInDays: 30, + PATExpireInDays: intPtr(30), }) require.Error(t, err) @@ -93,6 +161,8 @@ func TestSetupOwner_CreatePATFails_RollsBackSetupAccountAndUser(t *testing.T) { } func TestSetupOwner_CreatePATFails_AccountAlreadyGoneStillRollsBackUser(t *testing.T) { + t.Setenv(SetupPATEnabledEnvKey, "true") + ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) accountStore.EXPECT().GetAccount(gomock.Any(), "acc-1").Return(nil, status.NewAccountNotFoundError("acc-1")) @@ -120,7 +190,7 @@ func TestSetupOwner_CreatePATFails_AccountAlreadyGoneStillRollsBackUser(t *testi result, err := setupManager.SetupOwner(context.Background(), "admin@example.com", "securepassword123", "Admin", SetupOptions{ CreatePAT: true, - PATExpireInDays: 30, + PATExpireInDays: intPtr(30), }) require.Error(t, err) @@ -130,6 +200,8 @@ func TestSetupOwner_CreatePATFails_AccountAlreadyGoneStillRollsBackUser(t *testi } func TestSetupOwner_CreatePATFails_AccountRollbackFailureStillRollsBackUser(t *testing.T) { + t.Setenv(SetupPATEnabledEnvKey, "true") + ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) account := &types.Account{Id: "acc-1"} @@ -159,7 +231,7 @@ func TestSetupOwner_CreatePATFails_AccountRollbackFailureStillRollsBackUser(t *t result, err := setupManager.SetupOwner(context.Background(), "admin@example.com", "securepassword123", "Admin", SetupOptions{ CreatePAT: true, - PATExpireInDays: 30, + PATExpireInDays: intPtr(30), }) require.Error(t, err) diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 9e92ad29d..72a5aef2b 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -3430,10 +3430,11 @@ components: type: boolean example: true pat_expire_in: - description: Expiration of the Personal Access Token in days. Required when create_pat is true. + description: Expiration of the Personal Access Token in days. Defaults to 1 day when omitted. type: integer minimum: 1 maximum: 365 + default: 1 example: 30 required: - email @@ -4996,7 +4997,7 @@ paths: description: | Creates the initial admin user for the instance. This endpoint does not require authentication but only works when setup is required (no accounts exist and embedded IDP is enabled). - When the management server is started with `NB_SETUP_PAT_ENABLED=true` and the request includes `create_pat: true` together with `pat_expire_in`, the endpoint also provisions the NetBird account for the new owner user and returns the plain text Personal Access Token in `personal_access_token`. If any post-user step fails the Dex user is rolled back and setup remains retryable. + When the management server is started with `NB_SETUP_PAT_ENABLED=true` and the request includes `create_pat: true`, the endpoint also provisions the NetBird account for the new owner user and returns the plain text Personal Access Token in `personal_access_token`. The optional `pat_expire_in` value defaults to 1 day when omitted. If any post-user step fails the Dex user is rolled back and setup remains retryable. tags: [ Instance ] security: [ ] requestBody: diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index fa6298e1c..41ea39717 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -4306,7 +4306,7 @@ type SetupRequest struct { // Password Password for the admin user (minimum 8 characters) Password string `json:"password"` - // PatExpireIn Expiration of the Personal Access Token in days. Required when create_pat is true. + // PatExpireIn Expiration of the Personal Access Token in days. Defaults to 1 day when omitted. PatExpireIn *int `json:"pat_expire_in,omitempty"` }