Files
netbird/management/server/users/manager.go
T
mlsmaycon 356f6bdda0 [management] Record proxy logins and mesh activity for active-user accounting
Activity accounting counts a user as active from their last login or from a
peer of theirs being seen. Neither timestamp moved when someone reached a
service through the reverse proxy, so a person who only ever uses
proxy-protected services and never opens the dashboard has no login on
record at all and is skipped outright.

The two proxy entry points mean different things, so they write different
things. GenerateSessionToken is only reached after an ID token was verified,
so a completed SSO sign-in records a login on the user. ValidateTunnelPeer
authorises by tunnel IP with no IdP involved, so it records that the peer
was seen instead; the owner counts through that. Both write on the granted
path only, in UTC, and log and drop failures — no authorisation decision
reads them back.

The peer write is throttled to once an hour against the peer already in
hand, so a busy peer does not rewrite its row behind every request. Both
store methods update one column and leave the session-ownership fields to
the sync stream that owns them.

Peers that accounting excludes, embedded proxy peers and browser clients,
are skipped rather than written for nothing.
2026-08-09 07:31:18 +00:00

96 lines
3.0 KiB
Go

package users
import (
"context"
"errors"
"time"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
)
type Manager interface {
GetUser(ctx context.Context, userID string) (*types.User, error)
GetUserWithGroups(ctx context.Context, userID string) (*types.User, []*types.Group, error)
// RefreshLastLogin records an interactive login on the user without
// rewriting the rest of the row, keeping a newer stored timestamp.
RefreshLastLogin(ctx context.Context, accountID, userID string, loginAt time.Time) error
}
type managerImpl struct {
store store.Store
}
type managerMock struct {
}
func NewManager(store store.Store) Manager {
return &managerImpl{
store: store,
}
}
func (m *managerImpl) RefreshLastLogin(ctx context.Context, accountID, userID string, loginAt time.Time) error {
return m.store.RefreshUserLastLogin(ctx, accountID, userID, loginAt)
}
func (m *managerImpl) GetUser(ctx context.Context, userID string) (*types.User, error) {
return m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
}
// GetUserWithGroups returns the user and the *types.Group records for the user's AutoGroups, in the same order as
// AutoGroups. Group ids that don't resolve to a stored group are skipped from the returned slice (the parallel id list is
// derivable from the returned User). Wraps two store calls today; can be optimised to a single JOIN later if needed.
// Any store error returns (nil, nil, err) so callers never receive a valid user alongside a non-nil error.
func (m *managerImpl) GetUserWithGroups(ctx context.Context, userID string) (*types.User, []*types.Group, error) {
user, err := m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
if err != nil {
return nil, nil, err
}
if len(user.AutoGroups) == 0 {
return user, nil, nil
}
groupsMap, err := m.store.GetGroupsByIDs(ctx, store.LockingStrengthNone, user.AccountID, user.AutoGroups)
if err != nil {
return nil, nil, err
}
groups := make([]*types.Group, 0, len(user.AutoGroups))
for _, id := range user.AutoGroups {
if g, ok := groupsMap[id]; ok && g != nil {
groups = append(groups, g)
}
}
return user, groups, nil
}
func NewManagerMock() Manager {
return &managerMock{}
}
func (m *managerMock) GetUser(ctx context.Context, userID string) (*types.User, error) {
switch userID {
case "adminUser":
return &types.User{Id: userID, Role: types.UserRoleAdmin}, nil
case "regularUser":
return &types.User{Id: userID, Role: types.UserRoleUser}, nil
case "ownerUser":
return &types.User{Id: userID, Role: types.UserRoleOwner}, nil
case "billingUser":
return &types.User{Id: userID, Role: types.UserRoleBillingAdmin}, nil
default:
return nil, errors.New("user not found")
}
}
func (m *managerMock) RefreshLastLogin(_ context.Context, _, _ string, _ time.Time) error {
return nil
}
func (m *managerMock) GetUserWithGroups(ctx context.Context, userID string) (*types.User, []*types.Group, error) {
user, err := m.GetUser(ctx, userID)
if err != nil {
return nil, nil, err
}
return user, nil, nil
}