Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f28b086
Add user invite link feature for embedded IdP
braginini Jan 22, 2026
c4cf97c
Add OpenAPI definitions for new endpoints
braginini Jan 22, 2026
00306ea
Add best-effort IdP user rollback
braginini Jan 22, 2026
d02cc3c
Fix OpenAI definition
braginini Jan 22, 2026
114c362
Add invited by to invite info and account invites endpoint
braginini Jan 23, 2026
9c5d62d
Regenerate invite by ID
braginini Jan 23, 2026
8e56427
Add invite delete
braginini Jan 24, 2026
7ed5887
Add missing activities
braginini Jan 24, 2026
2229f40
The bypass path for invite acceptance is now more restrictive
braginini Jan 24, 2026
7e41d68
Don't swallow JSON malformed requests in invites
braginini Jan 24, 2026
39698e7
Don't ignore store errors when looking up invites
braginini Jan 24, 2026
32a8f5f
Add password constraints
braginini Jan 24, 2026
05a895f
Fix checksum padding to avoid spaces in tokens.
braginini Jan 24, 2026
ea83e54
Add password validation
braginini Jan 24, 2026
3806c33
Add invites handler tests
braginini Jan 24, 2026
26d369a
Add invites manager test
braginini Jan 24, 2026
59de054
Update invite instead of creating a new one when regenerating
braginini Jan 24, 2026
54ae2a4
Fix lint
braginini Jan 24, 2026
2de4c1a
Add rate limiter
braginini Jan 24, 2026
a5dd162
Rate limiter only considers remote addr
braginini Jan 25, 2026
97b9010
Add rate limiter test
braginini Jan 25, 2026
e6ec1c0
Add Istance Version Endpoints (#5179)
braginini Jan 25, 2026
ab4d1e4
Remove unnecessary HTTP methods check
braginini Jan 26, 2026
b525382
Unify response handling
braginini Jan 26, 2026
f28561f
Rename invite_link to invite_token
braginini Jan 26, 2026
abb5363
Fix tests
braginini Jan 26, 2026
c6608bd
add store tests for invites
braginini Jan 26, 2026
d6606d5
Remove unnecessary transaction when regenerating invite
braginini Jan 26, 2026
607ad52
Add user invite test
braginini Jan 26, 2026
d43f8af
Add minimum invite expiration
braginini Jan 26, 2026
a664640
Add DELETE invite endpoint definition
braginini Jan 26, 2026
0af74f4
Make error handling consistent
braginini Jan 26, 2026
e44693a
Remove unused DELETE method check
braginini Jan 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions management/server/account/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ type Manager interface {
autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error)
CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error)
CreateUserInvite(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error)
AcceptUserInvite(ctx context.Context, token, password string) error
RegenerateUserInvite(ctx context.Context, accountID, initiatorUserID, inviteID string, expiresIn int) (*types.UserInvite, error)
GetUserInviteInfo(ctx context.Context, token string) (*types.UserInviteInfo, error)
ListUserInvites(ctx context.Context, accountID, initiatorUserID string) ([]*types.UserInvite, error)
DeleteUserInvite(ctx context.Context, accountID, initiatorUserID, inviteID string) error
DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error
DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error
UpdateUserPassword(ctx context.Context, accountID, currentUserID, targetUserID string, oldPassword, newPassword string) error
Expand Down
10 changes: 10 additions & 0 deletions management/server/activity/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ const (

UserPasswordChanged Activity = 103

UserInviteLinkCreated Activity = 104
UserInviteLinkAccepted Activity = 105
UserInviteLinkRegenerated Activity = 106
UserInviteLinkDeleted Activity = 107

AccountDeleted Activity = 99999
)

Expand Down Expand Up @@ -327,6 +332,11 @@ var activityMap = map[Activity]Code{
JobCreatedByUser: {"Create Job for peer", "peer.job.create"},

UserPasswordChanged: {"User password changed", "user.password.change"},

UserInviteLinkCreated: {"User invite link created", "user.invite.link.create"},
UserInviteLinkAccepted: {"User invite link accepted", "user.invite.link.accept"},
UserInviteLinkRegenerated: {"User invite link regenerated", "user.invite.link.regenerate"},
UserInviteLinkDeleted: {"User invite link deleted", "user.invite.link.delete"},
}

// StringCode returns a string code of the activity
Expand Down
10 changes: 10 additions & 0 deletions management/server/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
if err := bypass.AddBypassPath("/api/setup"); err != nil {
return nil, fmt.Errorf("failed to add bypass path: %w", err)
}
// Public invite endpoints (tokens start with nbi_)
if err := bypass.AddBypassPath("/api/users/invites/nbi_*"); err != nil {
return nil, fmt.Errorf("failed to add bypass path: %w", err)
}
if err := bypass.AddBypassPath("/api/users/invites/nbi_*/accept"); err != nil {
return nil, fmt.Errorf("failed to add bypass path: %w", err)
}

var rateLimitingConfig *middleware.RateLimiterConfig
if os.Getenv(rateLimitingEnabledKey) == "true" {
Expand Down Expand Up @@ -132,6 +139,8 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
accounts.AddEndpoints(accountManager, settingsManager, embeddedIdpEnabled, router)
peers.AddEndpoints(accountManager, router, networkMapController)
users.AddEndpoints(accountManager, router)
users.AddInvitesEndpoints(accountManager, router)
users.AddPublicInvitesEndpoints(accountManager, router)
setup_keys.AddEndpoints(accountManager, router)
policies.AddEndpoints(accountManager, LocationManager, router)
policies.AddPostureCheckEndpoints(accountManager, LocationManager, router)
Expand All @@ -145,6 +154,7 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
recordsManager.RegisterEndpoints(router, rManager)
idp.AddEndpoints(accountManager, router)
instance.AddEndpoints(instanceManager, router)
instance.AddVersionEndpoint(instanceManager, router)

// Mount embedded IdP handler at /oauth2 path if configured
if embeddedIdpEnabled {
Expand Down
35 changes: 35 additions & 0 deletions management/server/http/handlers/instance/instance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ func AddEndpoints(instanceManager nbinstance.Manager, router *mux.Router) {
router.HandleFunc("/setup", h.setup).Methods("POST", "OPTIONS")
}

// AddVersionEndpoint registers the authenticated version endpoint.
func AddVersionEndpoint(instanceManager nbinstance.Manager, router *mux.Router) {
h := &handler{
instanceManager: instanceManager,
}

router.HandleFunc("/instance/version", h.getVersionInfo).Methods("GET", "OPTIONS")
}

// getInstanceStatus returns the instance status including whether setup is required.
// This endpoint is unauthenticated.
func (h *handler) getInstanceStatus(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -65,3 +74,29 @@ func (h *handler) setup(w http.ResponseWriter, r *http.Request) {
Email: userData.Email,
})
}

// getVersionInfo returns version information for NetBird components.
// This endpoint requires authentication.
func (h *handler) getVersionInfo(w http.ResponseWriter, r *http.Request) {
versionInfo, err := h.instanceManager.GetVersionInfo(r.Context())
if err != nil {
log.WithContext(r.Context()).Errorf("failed to get version info: %v", err)
util.WriteErrorResponse("failed to get version info", http.StatusInternalServerError, w)
return
}

resp := api.InstanceVersionInfo{
ManagementCurrentVersion: versionInfo.CurrentVersion,
ManagementUpdateAvailable: versionInfo.ManagementUpdateAvailable,
}

if versionInfo.DashboardVersion != "" {
resp.DashboardAvailableVersion = &versionInfo.DashboardVersion
}

if versionInfo.ManagementVersion != "" {
resp.ManagementAvailableVersion = &versionInfo.ManagementVersion
}

util.WriteJSONObject(r.Context(), w, resp)
}
54 changes: 54 additions & 0 deletions management/server/http/handlers/instance/instance_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type mockInstanceManager struct {
isSetupRequired bool
isSetupRequiredFn func(ctx context.Context) (bool, error)
createOwnerUserFn func(ctx context.Context, email, password, name string) (*idp.UserData, error)
getVersionInfoFn func(ctx context.Context) (*nbinstance.VersionInfo, error)
}

func (m *mockInstanceManager) IsSetupRequired(ctx context.Context) (bool, error) {
Expand Down Expand Up @@ -66,6 +67,18 @@ func (m *mockInstanceManager) CreateOwnerUser(ctx context.Context, email, passwo
}, nil
}

func (m *mockInstanceManager) GetVersionInfo(ctx context.Context) (*nbinstance.VersionInfo, error) {
if m.getVersionInfoFn != nil {
return m.getVersionInfoFn(ctx)
}
return &nbinstance.VersionInfo{
CurrentVersion: "0.34.0",
DashboardVersion: "2.0.0",
ManagementVersion: "0.35.0",
ManagementUpdateAvailable: true,
}, nil
}

var _ nbinstance.Manager = (*mockInstanceManager)(nil)

func setupTestRouter(manager nbinstance.Manager) *mux.Router {
Expand Down Expand Up @@ -279,3 +292,44 @@ func TestSetup_ManagerError(t *testing.T) {

assert.Equal(t, http.StatusInternalServerError, rec.Code)
}

func TestGetVersionInfo_Success(t *testing.T) {
manager := &mockInstanceManager{}
router := mux.NewRouter()
AddVersionEndpoint(manager, router)

req := httptest.NewRequest(http.MethodGet, "/instance/version", nil)
rec := httptest.NewRecorder()

router.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)

var response api.InstanceVersionInfo
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)

assert.Equal(t, "0.34.0", response.ManagementCurrentVersion)
assert.NotNil(t, response.DashboardAvailableVersion)
assert.Equal(t, "2.0.0", *response.DashboardAvailableVersion)
assert.NotNil(t, response.ManagementAvailableVersion)
assert.Equal(t, "0.35.0", *response.ManagementAvailableVersion)
assert.True(t, response.ManagementUpdateAvailable)
}

func TestGetVersionInfo_Error(t *testing.T) {
manager := &mockInstanceManager{
getVersionInfoFn: func(ctx context.Context) (*nbinstance.VersionInfo, error) {
return nil, errors.New("failed to fetch versions")
},
}
router := mux.NewRouter()
AddVersionEndpoint(manager, router)

req := httptest.NewRequest(http.MethodGet, "/instance/version", nil)
rec := httptest.NewRecorder()

router.ServeHTTP(rec, req)

assert.Equal(t, http.StatusInternalServerError, rec.Code)
}
Loading
Loading