Add system activity tracking and event store (#636)

This PR adds system activity tracking. 
The management service records events like 
add/remove peer,  group, rule, route, etc.

The activity events are stored in the SQLite event store
and can be queried by the HTTP API.
This commit is contained in:
Misha Bragin 2023-01-02 15:11:32 +01:00 committed by GitHub
parent 50caacff69
commit 5c0b8a46f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1827 additions and 227 deletions

View File

@ -31,13 +31,13 @@ jobs:
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib
- name: Install modules
run: go mod tidy
- name: Test
run: GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
test_client_on_docker:
runs-on: ubuntu-latest

View File

@ -2,6 +2,7 @@ package cmd
import (
"context"
"github.com/netbirdio/netbird/management/server/activity"
"net"
"path/filepath"
"testing"
@ -68,7 +69,12 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "")
eventStore := &activity.InMemoryEventStore{}
if err != nil {
return nil, nil
}
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore)
if err != nil {
t.Fatal(err)
}

View File

@ -9,6 +9,7 @@ import (
nbstatus "github.com/netbirdio/netbird/client/status"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/route"
"github.com/stretchr/testify/assert"
"net"
@ -953,7 +954,12 @@ func startManagement(port int, dataDir string) (*grpc.Server, error) {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "")
eventStore := &activity.InMemoryEventStore{}
if err != nil {
return nil, nil
}
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore)
if err != nil {
return nil, err
}

1
go.mod
View File

@ -40,6 +40,7 @@ require (
github.com/hashicorp/go-version v1.6.0
github.com/libp2p/go-netroute v0.2.0
github.com/magiconair/properties v1.8.5
github.com/mattn/go-sqlite3 v1.14.16
github.com/miekg/dns v1.1.41
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/patrickmn/go-cache v2.1.0+incompatible

2
go.sum
View File

@ -432,6 +432,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=

View File

@ -2,6 +2,7 @@ package client
import (
"context"
"github.com/netbirdio/netbird/management/server/activity"
"net"
"path/filepath"
"sync"
@ -55,7 +56,9 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "")
eventStore := &activity.InMemoryEventStore{}
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore)
if err != nil {
t.Fatal(err)
}

View File

@ -9,6 +9,7 @@ import (
"fmt"
"github.com/google/uuid"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/management/server/activity/sqlite"
httpapi "github.com/netbirdio/netbird/management/server/http"
"github.com/netbirdio/netbird/management/server/metrics"
"github.com/netbirdio/netbird/management/server/telemetry"
@ -142,7 +143,12 @@ var (
if disableSingleAccMode {
mgmtSingleAccModeDomain = ""
}
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain, dnsDomain)
eventStore, err := sqlite.NewSQLiteStore(config.Datadir)
if err != nil {
return err
}
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain,
dnsDomain, eventStore)
if err != nil {
return fmt.Errorf("failed to build default manager: %v", err)
}
@ -251,6 +257,7 @@ var (
}
gRPCAPIHandler.Stop()
_ = store.Close()
_ = eventStore.Close()
log.Infof("stopped Management Service")
return nil

View File

@ -6,6 +6,7 @@ import (
"github.com/eko/gocache/v3/cache"
cacheStore "github.com/eko/gocache/v3/store"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
@ -39,11 +40,11 @@ func cacheEntryExpiration() time.Duration {
type AccountManager interface {
GetOrCreateAccountByUser(userId, domain string) (*Account, error)
CreateSetupKey(accountID string, keyName string, keyType SetupKeyType, expiresIn time.Duration,
autoGroups []string, usageLimit int) (*SetupKey, error)
SaveSetupKey(accountID string, key *SetupKey) (*SetupKey, error)
CreateUser(accountID string, key *UserInfo) (*UserInfo, error)
autoGroups []string, usageLimit int, userID string) (*SetupKey, error)
SaveSetupKey(accountID string, key *SetupKey, userID string) (*SetupKey, error)
CreateUser(accountID, userID string, key *UserInfo) (*UserInfo, error)
ListSetupKeys(accountID, userID string) ([]*SetupKey, error)
SaveUser(accountID string, key *User) (*UserInfo, error)
SaveUser(accountID, userID string, update *User) (*UserInfo, error)
GetSetupKey(accountID, userID, keyID string) (*SetupKey, error)
GetAccountByUserOrAccountID(userID, accountID, domain string) (*Account, error)
GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, *User, error)
@ -52,7 +53,7 @@ type AccountManager interface {
GetPeer(peerKey string) (*Peer, error)
GetPeers(accountID, userID string) ([]*Peer, error)
MarkPeerConnected(peerKey string, connected bool) error
DeletePeer(accountId string, peerKey string) (*Peer, error)
DeletePeer(accountID, peerKey, userID string) (*Peer, error)
GetPeerByIP(accountId string, peerIP string) (*Peer, error)
UpdatePeer(accountID string, peer *Peer) (*Peer, error)
GetNetworkMap(peerKey string) (*NetworkMap, error)
@ -62,7 +63,7 @@ type AccountManager interface {
UpdatePeerSSHKey(peerKey string, sshKey string) error
GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error)
GetGroup(accountId, groupID string) (*Group, error)
SaveGroup(accountId string, group *Group) error
SaveGroup(accountID, userID string, group *Group) error
UpdateGroup(accountID string, groupID string, operations []GroupUpdateOperation) (*Group, error)
DeleteGroup(accountId, groupID string) error
ListGroups(accountId string) ([]*Group, error)
@ -70,9 +71,9 @@ type AccountManager interface {
GroupDeletePeer(accountId, groupID, peerKey string) error
GroupListPeers(accountId, groupID string) ([]*Peer, error)
GetRule(accountID, ruleID, userID string) (*Rule, error)
SaveRule(accountID string, rule *Rule) error
SaveRule(accountID, userID string, rule *Rule) error
UpdateRule(accountID string, ruleID string, operations []RuleUpdateOperation) (*Rule, error)
DeleteRule(accountId, ruleID string) error
DeleteRule(accountID, ruleID, userID string) error
ListRules(accountID, userID string) ([]*Rule, error)
GetRoute(accountID, routeID, userID string) (*route.Route, error)
CreateRoute(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error)
@ -87,6 +88,7 @@ type AccountManager interface {
DeleteNameServerGroup(accountID, nsGroupID string) error
ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error)
GetDNSDomain() string
GetEvents(accountID, userID string) ([]*activity.Event, error)
}
type DefaultAccountManager struct {
@ -99,6 +101,7 @@ type DefaultAccountManager struct {
idpManager idp.Manager
cacheManager cache.CacheInterface[[]*idp.UserData]
ctx context.Context
eventStore activity.Store
// singleAccountMode indicates whether the instance has a single account.
// If true, then every new user will end up under the same account.
@ -251,6 +254,11 @@ func (a *Account) GetPeerRules(peerPubKey string) (srcRules []*Rule, dstRules []
return srcRules, dstRules
}
// GetGroup returns a group by ID if exists, nil otherwise
func (a *Account) GetGroup(groupID string) *Group {
return a.Groups[groupID]
}
// GetPeers returns a list of all Account peers
func (a *Account) GetPeers() []*Peer {
var peers []*Peer
@ -435,7 +443,7 @@ func (a *Account) GetGroupAll() (*Group, error) {
// BuildManager creates a new DefaultAccountManager with a provided Store
func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager,
singleAccountModeDomain string, dnsDomain string) (*DefaultAccountManager, error) {
singleAccountModeDomain string, dnsDomain string, eventStore activity.Store) (*DefaultAccountManager, error) {
am := &DefaultAccountManager{
Store: store,
peersUpdateManager: peersUpdateManager,
@ -444,6 +452,7 @@ func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManage
cacheMux: sync.Mutex{},
cacheLoading: map[string]chan struct{}{},
dnsDomain: dnsDomain,
eventStore: eventStore,
}
allAccounts := store.GetAllAccounts()
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
@ -510,7 +519,18 @@ func (am *DefaultAccountManager) newAccount(userID, domain string) (*Account, er
log.Warnf("an account with ID already exists, retrying...")
continue
} else if statusErr.Type() == status.NotFound {
return newAccountWithId(accountId, userID, domain), nil
newAccount := newAccountWithId(accountId, userID, domain)
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.AccountCreated,
AccountID: newAccount.Id,
TargetID: newAccount.Id,
InitiatorID: userID,
})
if err != nil {
return nil, err
}
return newAccount, nil
} else {
return nil, err
}
@ -797,6 +817,19 @@ func (am *DefaultAccountManager) handleNewUserAccount(domainAcc *Account, claims
return nil, err
}
event := &activity.Event{
Timestamp: time.Now(),
Activity: activity.UserJoined,
AccountID: account.Id,
TargetID: claims.UserId,
InitiatorID: claims.UserId,
}
_, err = am.eventStore.Save(event)
if err != nil {
return nil, err
}
return account, nil
}
@ -828,6 +861,17 @@ func (am *DefaultAccountManager) redeemInvite(account *Account, userID string) e
return
}
log.Debugf("user %s of account %s redeemed invite", user.ID, account.Id)
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.UserJoined,
AccountID: account.Id,
TargetID: userID,
InitiatorID: userID,
})
if err != nil {
log.Warnf("failed saving activity event %v", err)
return
}
}()
}

View File

@ -3,6 +3,7 @@ package server
import (
"fmt"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/route"
"net"
"reflect"
@ -112,23 +113,43 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
return
}
userId := "test_user"
account, err := manager.GetOrCreateAccountByUser(userId, "")
account, err := manager.GetOrCreateAccountByUser(userID, "")
if err != nil {
t.Fatal(err)
}
if account == nil {
t.Fatalf("expected to create an account for a user %s", userId)
t.Fatalf("expected to create an account for a user %s", userID)
return
}
account, err = manager.Store.GetAccountByUser(userId)
account, err = manager.Store.GetAccountByUser(userID)
if err != nil {
t.Errorf("expected to get existing account after creation, no account was found for a user %s", userId)
t.Errorf("expected to get existing account after creation, no account was found for a user %s", userID)
return
}
if account != nil && account.Users[userId] == nil {
t.Fatalf("expected to create an account for a user %s but no user was found after creation udner the account %s", userId, account.Id)
if account != nil && account.Users[userID] == nil {
t.Fatalf("expected to create an account for a user %s but no user was found after creation udner the account %s", userID, account.Id)
return
}
// check the corresponding events that should have been generated
events, err := manager.GetEvents(account.Id, userID)
if err != nil {
return
}
var ev *activity.Event
for _, event := range events {
if event.Activity == activity.AccountCreated {
ev = event
}
}
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, userID, ev.InitiatorID)
assert.Equal(t, account.Id, ev.TargetID)
}
func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
@ -500,7 +521,7 @@ func TestAccountManager_AddPeer(t *testing.T) {
return
}
account, err := createAccount(manager, "test_account", "account_creator", "")
account, err := createAccount(manager, "test_account", "account_creator", "netbird.cloud")
if err != nil {
t.Fatal(err)
}
@ -561,6 +582,27 @@ func TestAccountManager_AddPeer(t *testing.T) {
if account.Network.CurrentSerial() != 1 {
t.Errorf("expecting Network Serial=%d to be incremented by 1 and be equal to %d when adding new peer to account", serial, account.Network.CurrentSerial())
}
// check the corresponding events that should have been generated
events, err := manager.GetEvents(account.Id, userID)
if err != nil {
return
}
var ev *activity.Event
for _, event := range events {
if event.Activity == activity.PeerAddedWithSetupKey {
ev = event
}
}
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, peer.Name, ev.Meta["name"])
assert.Equal(t, peer.FQDN(account.Domain), ev.Meta["fqdn"])
assert.Equal(t, setupKey.Id, ev.InitiatorID)
assert.Equal(t, peer.IP.String(), ev.TargetID)
assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"]))
}
func TestAccountManager_AddPeerWithUserID(t *testing.T) {
@ -570,9 +612,7 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
return
}
userId := "account_creator"
account, err := manager.GetOrCreateAccountByUser(userId, "")
account, err := manager.GetOrCreateAccountByUser(userID, "netbird.cloud")
if err != nil {
t.Fatal(err)
}
@ -590,9 +630,9 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
return
}
expectedPeerKey := key.PublicKey().String()
expectedUserId := userId
expectedUserID := userID
peer, err := manager.AddPeer("", userId, &Peer{
peer, err := manager.AddPeer("", userID, &Peer{
Key: expectedPeerKey,
Meta: PeerSystemMeta{},
Name: expectedPeerKey,
@ -616,13 +656,34 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
}
if peer.UserID != expectedUserId {
t.Errorf("expecting just added peer to have UserID = %s, got %s", expectedUserId, peer.UserID)
if peer.UserID != expectedUserID {
t.Errorf("expecting just added peer to have UserID = %s, got %s", expectedUserID, peer.UserID)
}
if account.Network.CurrentSerial() != 1 {
t.Errorf("expecting Network Serial=%d to be incremented by 1 and be equal to %d when adding new peer to account", serial, account.Network.CurrentSerial())
}
// check the corresponding events that should have been generated
events, err := manager.GetEvents(account.Id, userID)
if err != nil {
return
}
var ev *activity.Event
for _, event := range events {
if event.Activity == activity.PeerAddedByUser {
ev = event
}
}
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, peer.Name, ev.Meta["name"])
assert.Equal(t, peer.FQDN(account.Domain), ev.Meta["fqdn"])
assert.Equal(t, userID, ev.InitiatorID)
assert.Equal(t, peer.IP.String(), ev.TargetID)
assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"]))
}
func TestAccountManager_NetworkUpdates(t *testing.T) {
@ -632,7 +693,9 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
return
}
account, err := createAccount(manager, "test_account", "account_creator", "")
userID := "account_creator"
account, err := createAccount(manager, "test_account", userID, "")
if err != nil {
t.Fatal(err)
}
@ -714,7 +777,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
}
}()
if err := manager.SaveGroup(account.Id, &group); err != nil {
if err := manager.SaveGroup(account.Id, userID, &group); err != nil {
t.Errorf("save group: %v", err)
return
}
@ -739,7 +802,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
defaultRule = r
}
if err := manager.DeleteRule(account.Id, defaultRule.ID); err != nil {
if err := manager.DeleteRule(account.Id, defaultRule.ID, userID); err != nil {
t.Errorf("delete default rule: %v", err)
return
}
@ -759,7 +822,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
}
}()
if err := manager.SaveRule(account.Id, &rule); err != nil {
if err := manager.SaveRule(account.Id, userID, &rule); err != nil {
t.Errorf("delete default rule: %v", err)
return
}
@ -779,7 +842,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
}
}()
if _, err := manager.DeletePeer(account.Id, peer3.Key); err != nil {
if _, err := manager.DeletePeer(account.Id, peer3.Key, userID); err != nil {
t.Errorf("delete peer: %v", err)
return
}
@ -814,8 +877,8 @@ func TestAccountManager_DeletePeer(t *testing.T) {
t.Fatal(err)
return
}
account, err := createAccount(manager, "test_account", "account_creator", "")
userID := "account_creator"
account, err := createAccount(manager, "test_account", userID, "netbird.cloud")
if err != nil {
t.Fatal(err)
}
@ -833,7 +896,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
peerKey := key.PublicKey().String()
_, err = manager.AddPeer(setupKey.Key, "", &Peer{
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
Key: peerKey,
Meta: PeerSystemMeta{},
Name: peerKey,
@ -843,7 +906,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
return
}
_, err = manager.DeletePeer(account.Id, peerKey)
_, err = manager.DeletePeer(account.Id, peerKey, userID)
if err != nil {
return
}
@ -857,6 +920,27 @@ func TestAccountManager_DeletePeer(t *testing.T) {
if account.Network.CurrentSerial() != 2 {
t.Errorf("expecting Network Serial=%d to be incremented and be equal to 2 after adding and deleteing a peer", account.Network.CurrentSerial())
}
// check the corresponding events that should have been generated
events, err := manager.GetEvents(account.Id, userID)
if err != nil {
return
}
var ev *activity.Event
for _, event := range events {
if event.Activity == activity.PeerRemovedByUser {
ev = event
}
}
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, peer.Name, ev.Meta["name"])
assert.Equal(t, peer.FQDN(account.Domain), ev.Meta["fqdn"])
assert.Equal(t, userID, ev.InitiatorID)
assert.Equal(t, peer.IP.String(), ev.TargetID)
assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"]))
}
func TestGetUsersFromAccount(t *testing.T) {
@ -1228,7 +1312,8 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) {
if err != nil {
return nil, err
}
return BuildManager(store, NewPeersUpdateManager(), nil, "", "")
eventStore := &activity.InMemoryEventStore{}
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore)
}
func createStore(t *testing.T) (Store, error) {

View File

@ -0,0 +1,202 @@
package activity
const (
// PeerAddedByUser indicates that a user added a new peer to the system
PeerAddedByUser Activity = iota
// PeerAddedWithSetupKey indicates that a new peer joined the system using a setup key
PeerAddedWithSetupKey
// UserJoined indicates that a new user joined the account
UserJoined
// UserInvited indicates that a new user was invited to join the account
UserInvited
// AccountCreated indicates that a new account has been created
AccountCreated
// PeerRemovedByUser indicates that a user removed a peer from the system
PeerRemovedByUser
// RuleAdded indicates that a user added a new rule
RuleAdded
// RuleUpdated indicates that a user updated a rule
RuleUpdated
// RuleRemoved indicates that a user removed a rule
RuleRemoved
// SetupKeyCreated indicates that a user created a new setup key
SetupKeyCreated
// SetupKeyUpdated indicates that a user updated a setup key
SetupKeyUpdated
// SetupKeyRevoked indicates that a user revoked a setup key
SetupKeyRevoked
// SetupKeyOverused indicates that setup key usage exhausted
SetupKeyOverused
// GroupCreated indicates that a user created a group
GroupCreated
// GroupUpdated indicates that a user updated a group
GroupUpdated
// GroupAddedToPeer indicates that a user added group to a peer
GroupAddedToPeer
// GroupRemovedFromPeer indicates that a user removed peer group
GroupRemovedFromPeer
// GroupAddedToUser indicates that a user added group to a user
GroupAddedToUser
// GroupRemovedFromUser indicates that a user removed a group from a user
GroupRemovedFromUser
// UserRoleUpdated indicates that a user changed the role of a user
UserRoleUpdated
// GroupAddedToSetupKey indicates that a user added group to a setup key
GroupAddedToSetupKey
// GroupRemovedFromSetupKey indicates that a user removed a group from a setup key
GroupRemovedFromSetupKey
)
const (
// PeerAddedByUserMessage is a human-readable text message of the PeerAddedByUser activity
PeerAddedByUserMessage string = "Peer added"
// PeerAddedWithSetupKeyMessage is a human-readable text message of the PeerAddedWithSetupKey activity
PeerAddedWithSetupKeyMessage = PeerAddedByUserMessage
//UserJoinedMessage is a human-readable text message of the UserJoined activity
UserJoinedMessage string = "User joined"
//UserInvitedMessage is a human-readable text message of the UserInvited activity
UserInvitedMessage string = "User invited"
//AccountCreatedMessage is a human-readable text message of the AccountCreated activity
AccountCreatedMessage string = "Account created"
// PeerRemovedByUserMessage is a human-readable text message of the PeerRemovedByUser activity
PeerRemovedByUserMessage string = "Peer deleted"
// RuleAddedMessage is a human-readable text message of the RuleAdded activity
RuleAddedMessage string = "Rule added"
// RuleRemovedMessage is a human-readable text message of the RuleRemoved activity
RuleRemovedMessage string = "Rule deleted"
// RuleUpdatedMessage is a human-readable text message of the RuleRemoved activity
RuleUpdatedMessage string = "Rule updated"
// SetupKeyCreatedMessage is a human-readable text message of the SetupKeyCreated activity
SetupKeyCreatedMessage string = "Setup key created"
// SetupKeyUpdatedMessage is a human-readable text message of the SetupKeyUpdated activity
SetupKeyUpdatedMessage string = "Setup key updated"
// SetupKeyRevokedMessage is a human-readable text message of the SetupKeyRevoked activity
SetupKeyRevokedMessage string = "Setup key revoked"
// SetupKeyOverusedMessage is a human-readable text message of the SetupKeyOverused activity
SetupKeyOverusedMessage string = "Setup key overused"
// GroupCreatedMessage is a human-readable text message of the GroupCreated activity
GroupCreatedMessage string = "Group created"
// GroupUpdatedMessage is a human-readable text message of the GroupUpdated activity
GroupUpdatedMessage string = "Group updated"
// GroupAddedToPeerMessage is a human-readable text message of the GroupAddedToPeer activity
GroupAddedToPeerMessage string = "Group added to peer"
// GroupRemovedFromPeerMessage is a human-readable text message of the GroupRemovedFromPeer activity
GroupRemovedFromPeerMessage string = "Group removed from peer"
// GroupAddedToUserMessage is a human-readable text message of the GroupAddedToUser activity
GroupAddedToUserMessage string = "Group added to user"
// GroupRemovedFromUserMessage is a human-readable text message of the GroupRemovedFromUser activity
GroupRemovedFromUserMessage string = "Group removed from user"
// UserRoleUpdatedMessage is a human-readable text message of the UserRoleUpdatedMessage activity
UserRoleUpdatedMessage string = "User role updated"
// GroupAddedToSetupKeyMessage is a human-readable text message of the GroupAddedToSetupKey activity
GroupAddedToSetupKeyMessage string = "Group added to setup key"
// GroupRemovedFromSetupKeyMessage is a human-readable text message of the GroupRemovedFromSetupKey activity
GroupRemovedFromSetupKeyMessage string = "Group removed from user setup key"
)
// Activity that triggered an Event
type Activity int
// Message returns a string representation of an activity
func (a Activity) Message() string {
switch a {
case PeerAddedByUser:
return PeerAddedByUserMessage
case PeerRemovedByUser:
return PeerRemovedByUserMessage
case PeerAddedWithSetupKey:
return PeerAddedWithSetupKeyMessage
case UserJoined:
return UserJoinedMessage
case UserInvited:
return UserInvitedMessage
case AccountCreated:
return AccountCreatedMessage
case RuleAdded:
return RuleAddedMessage
case RuleRemoved:
return RuleRemovedMessage
case RuleUpdated:
return RuleUpdatedMessage
case SetupKeyCreated:
return SetupKeyCreatedMessage
case SetupKeyUpdated:
return SetupKeyUpdatedMessage
case SetupKeyRevoked:
return SetupKeyRevokedMessage
case SetupKeyOverused:
return SetupKeyOverusedMessage
case GroupCreated:
return GroupCreatedMessage
case GroupUpdated:
return GroupUpdatedMessage
case GroupAddedToPeer:
return GroupAddedToPeerMessage
case GroupRemovedFromPeer:
return GroupRemovedFromPeerMessage
case GroupRemovedFromUser:
return GroupRemovedFromUserMessage
case GroupAddedToUser:
return GroupAddedToUserMessage
case UserRoleUpdated:
return UserRoleUpdatedMessage
case GroupAddedToSetupKey:
return GroupAddedToSetupKeyMessage
case GroupRemovedFromSetupKey:
return GroupRemovedFromSetupKeyMessage
default:
return "UNKNOWN_ACTIVITY"
}
}
// StringCode returns a string code of the activity
func (a Activity) StringCode() string {
switch a {
case PeerAddedByUser:
return "user.peer.add"
case PeerRemovedByUser:
return "user.peer.delete"
case PeerAddedWithSetupKey:
return "setupkey.peer.add"
case UserJoined:
return "user.join"
case UserInvited:
return "user.invite"
case AccountCreated:
return "account.create"
case RuleAdded:
return "rule.add"
case RuleRemoved:
return "rule.delete"
case RuleUpdated:
return "rule.update"
case SetupKeyCreated:
return "setupkey.add"
case SetupKeyRevoked:
return "setupkey.revoke"
case SetupKeyOverused:
return "setupkey.overuse"
case SetupKeyUpdated:
return "setupkey.update"
case GroupCreated:
return "group.add"
case GroupUpdated:
return "group.update"
case GroupRemovedFromPeer:
return "peer.group.delete"
case GroupAddedToPeer:
return "peer.group.add"
case GroupAddedToUser:
return "user.group.add"
case GroupRemovedFromUser:
return "user.group.delete"
case UserRoleUpdated:
return "user.role.update"
case GroupAddedToSetupKey:
return "setupkey.group.add"
case GroupRemovedFromSetupKey:
return "setupkey.group.delete"
default:
return "UNKNOWN_ACTIVITY"
}
}

View File

@ -0,0 +1,42 @@
package activity
import (
"time"
)
// Event represents a network/system activity event.
type Event struct {
// Timestamp of the event
Timestamp time.Time
// Activity that was performed during the event
Activity Activity
// ID of the event (can be empty, meaning that it wasn't yet generated)
ID uint64
// InitiatorID is the ID of an object that initiated the event (e.g., a user)
InitiatorID string
// TargetID is the ID of an object that was effected by the event (e.g., a peer)
TargetID string
// AccountID is the ID of an account where the event happened
AccountID string
// Meta of the event, e.g. deleted peer information like name, IP, etc
Meta map[string]any
}
// Copy the event
func (e *Event) Copy() *Event {
meta := make(map[string]any, len(e.Meta))
for key, value := range e.Meta {
meta[key] = value
}
return &Event{
Timestamp: e.Timestamp,
Activity: e.Activity,
ID: e.ID,
InitiatorID: e.InitiatorID,
TargetID: e.TargetID,
AccountID: e.AccountID,
Meta: meta,
}
}

View File

@ -0,0 +1,149 @@
package sqlite
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/activity"
// sqlite driver
_ "github.com/mattn/go-sqlite3"
"path/filepath"
"time"
)
const (
//eventSinkDB is the default name of the events database
eventSinkDB = "events.db"
createTableQuery = "CREATE TABLE IF NOT EXISTS events " +
"(id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"activity INTEGER, " +
"timestamp DATETIME, " +
"initiator_id TEXT," +
"account_id TEXT," +
"meta TEXT," +
" target_id TEXT);"
selectStatement = "SELECT id, activity, timestamp, initiator_id, target_id, account_id, meta" +
" FROM events WHERE account_id = ? ORDER BY timestamp %s LIMIT ? OFFSET ?;"
insertStatement = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " +
"VALUES(?, ?, ?, ?, ?, ?)"
)
// Store is the implementation of the activity.Store interface backed by SQLite
type Store struct {
db *sql.DB
}
// NewSQLiteStore creates a new Store with an event table if not exists.
func NewSQLiteStore(dataDir string) (*Store, error) {
dbFile := filepath.Join(dataDir, eventSinkDB)
db, err := sql.Open("sqlite3", dbFile)
if err != nil {
return nil, err
}
_, err = db.Exec(createTableQuery)
if err != nil {
return nil, err
}
return &Store{db: db}, nil
}
func processResult(result *sql.Rows) ([]*activity.Event, error) {
events := make([]*activity.Event, 0)
for result.Next() {
var id int64
var operation activity.Activity
var timestamp time.Time
var initiator string
var target string
var account string
var jsonMeta string
err := result.Scan(&id, &operation, &timestamp, &initiator, &target, &account, &jsonMeta)
if err != nil {
return nil, err
}
meta := make(map[string]any)
if jsonMeta != "" {
err = json.Unmarshal([]byte(jsonMeta), &meta)
if err != nil {
return nil, err
}
}
events = append(events, &activity.Event{
Timestamp: timestamp,
Activity: operation,
ID: uint64(id),
InitiatorID: initiator,
TargetID: target,
AccountID: account,
Meta: meta,
})
}
return events, nil
}
// Get returns "limit" number of events from index ordered descending or ascending by a timestamp
func (store *Store) Get(accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {
order := "DESC"
if !descending {
order = "ASC"
}
stmt, err := store.db.Prepare(fmt.Sprintf(selectStatement, order))
if err != nil {
return nil, err
}
result, err := stmt.Query(accountID, limit, offset)
if err != nil {
return nil, err
}
defer result.Close() //nolint
return processResult(result)
}
// Save an event in the SQLite events table
func (store *Store) Save(event *activity.Event) (*activity.Event, error) {
stmt, err := store.db.Prepare(insertStatement)
if err != nil {
return nil, err
}
var jsonMeta string
if event.Meta != nil {
metaBytes, err := json.Marshal(event.Meta)
if err != nil {
return nil, err
}
jsonMeta = string(metaBytes)
}
result, err := stmt.Exec(event.Activity, event.Timestamp, event.InitiatorID, event.TargetID, event.AccountID, jsonMeta)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
eventCopy := event.Copy()
eventCopy.ID = uint64(id)
return eventCopy, nil
}
// Close the Store
func (store *Store) Close() error {
if store.db != nil {
return store.db.Close()
}
return nil
}

View File

@ -0,0 +1,53 @@
package sqlite
import (
"fmt"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewSQLiteStore(t *testing.T) {
dataDir := t.TempDir()
store, err := NewSQLiteStore(dataDir)
if err != nil {
t.Fatal(err)
return
}
defer store.Close() //nolint
accountID := "account_1"
for i := 0; i < 10; i++ {
_, err = store.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.PeerAddedByUser,
InitiatorID: "user_" + fmt.Sprint(i),
TargetID: "peer_" + fmt.Sprint(i),
AccountID: accountID,
})
if err != nil {
t.Fatal(err)
return
}
}
result, err := store.Get(accountID, 0, 10, false)
if err != nil {
t.Fatal(err)
return
}
assert.Len(t, result, 10)
assert.True(t, result[0].Timestamp.Before(result[len(result)-1].Timestamp))
result, err = store.Get(accountID, 0, 5, true)
if err != nil {
t.Fatal(err)
return
}
assert.Len(t, result, 5)
assert.True(t, result[0].Timestamp.After(result[len(result)-1].Timestamp))
}

View File

@ -0,0 +1,54 @@
package activity
import "sync"
// Store provides an interface to store or stream events.
type Store interface {
// Save an event in the store
Save(event *Event) (*Event, error)
// Get returns "limit" number of events from the "offset" index ordered descending or ascending by a timestamp
Get(accountID string, offset, limit int, descending bool) ([]*Event, error)
// Close the sink flushing events if necessary
Close() error
}
// InMemoryEventStore implements the Store interface storing data in-memory
type InMemoryEventStore struct {
mu sync.Mutex
nextID uint64
events []*Event
}
// Save sets the Event.ID to 1
func (store *InMemoryEventStore) Save(event *Event) (*Event, error) {
store.mu.Lock()
defer store.mu.Unlock()
if store.events == nil {
store.events = make([]*Event, 0)
}
event.ID = store.nextID
store.nextID++
store.events = append(store.events, event)
return event, nil
}
// Get returns a list of ALL events that belong to the given accountID without taking offset, limit and order into consideration
func (store *InMemoryEventStore) Get(accountID string, offset, limit int, descending bool) ([]*Event, error) {
store.mu.Lock()
defer store.mu.Unlock()
events := make([]*Event, 0)
for _, event := range store.events {
if event.AccountID == accountID {
events = append(events, event)
}
}
return events, nil
}
// Close cleans up the event list
func (store *InMemoryEventStore) Close() error {
store.mu.Lock()
defer store.mu.Unlock()
store.events = make([]*Event, 0)
return nil
}

View File

@ -0,0 +1,33 @@
package server
import (
"fmt"
"github.com/netbirdio/netbird/management/server/activity"
)
// GetEvents returns a list of activity events of an account
func (am *DefaultAccountManager) GetEvents(accountID, userID string) ([]*activity.Event, error) {
events, err := am.eventStore.Get(accountID, 0, 10000, true)
if err != nil {
return nil, err
}
// this is a workaround for duplicate activity.UserJoined events that might occur when a user redeems invite.
// we will need to find a better way to handle this.
filtered := make([]*activity.Event, 0)
dups := make(map[string]struct{})
for _, event := range events {
if event.Activity == activity.UserJoined {
key := event.TargetID + event.InitiatorID + event.AccountID + fmt.Sprint(event.Activity)
_, duplicate := dups[key]
if duplicate {
continue
} else {
dups[key] = struct{}{}
}
}
filtered = append(filtered, event)
}
return filtered, nil
}

View File

@ -0,0 +1,63 @@
package server
import (
"github.com/netbirdio/netbird/management/server/activity"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func generateAndStoreEvents(t *testing.T, manager *DefaultAccountManager, typ activity.Activity, initiatorID, targetID,
accountID string, count int) {
for i := 0; i < count; i++ {
_, err := manager.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: typ,
InitiatorID: initiatorID,
TargetID: targetID,
AccountID: accountID,
})
if err != nil {
t.Fatal(err)
}
}
}
func TestDefaultAccountManager_GetEvents(t *testing.T) {
manager, err := createManager(t)
if err != nil {
return
}
accountID := "accountID"
t.Run("get empty events list", func(t *testing.T) {
events, err := manager.GetEvents(accountID, userID)
if err != nil {
return
}
assert.Len(t, events, 0)
_ = manager.eventStore.Close() //nolint
})
t.Run("get events", func(t *testing.T) {
generateAndStoreEvents(t, manager, activity.PeerAddedByUser, userID, "peer", accountID, 10)
events, err := manager.GetEvents(accountID, userID)
if err != nil {
return
}
assert.Len(t, events, 10)
_ = manager.eventStore.Close() //nolint
})
t.Run("get events without duplicates", func(t *testing.T) {
generateAndStoreEvents(t, manager, activity.UserJoined, userID, "", accountID, 10)
events, err := manager.GetEvents(accountID, userID)
if err != nil {
return
}
assert.Len(t, events, 1)
_ = manager.eventStore.Close() //nolint
})
}

View File

@ -1,6 +1,11 @@
package server
import "github.com/netbirdio/netbird/management/server/status"
import (
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/status"
log "github.com/sirupsen/logrus"
"time"
)
// Group of the peers for ACL
type Group struct {
@ -34,6 +39,11 @@ type GroupUpdateOperation struct {
Values []string
}
// EventMeta returns activity event meta related to the group
func (g *Group) EventMeta() map[string]any {
return map[string]any{"name": g.Name}
}
func (g *Group) Copy() *Group {
return &Group{
ID: g.ID,
@ -62,7 +72,7 @@ func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, er
}
// SaveGroup object of the peers
func (am *DefaultAccountManager) SaveGroup(accountID string, group *Group) error {
func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *Group) error {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -71,17 +81,95 @@ func (am *DefaultAccountManager) SaveGroup(accountID string, group *Group) error
if err != nil {
return err
}
account.Groups[group.ID] = group
oldGroup, exists := account.Groups[newGroup.ID]
account.Groups[newGroup.ID] = newGroup
account.Network.IncSerial()
if err = am.Store.SaveAccount(account); err != nil {
return err
}
if !exists {
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupCreated,
InitiatorID: userID,
TargetID: newGroup.ID,
AccountID: accountID,
Meta: newGroup.EventMeta(),
})
if err != nil {
return err
}
}
addedPeers := make([]string, 0)
removedPeers := make([]string, 0)
if !exists {
addedPeers = append(addedPeers, newGroup.Peers...)
} else {
addedPeers = difference(newGroup.Peers, oldGroup.Peers)
removedPeers = difference(oldGroup.Peers, newGroup.Peers)
}
for _, p := range addedPeers {
peer := account.Peers[p]
if peer == nil {
log.Errorf("peer %s not found under account %s while saving group", p, accountID)
continue
}
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupAddedToPeer,
InitiatorID: userID,
TargetID: peer.IP.String(),
AccountID: accountID,
Meta: map[string]any{"group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(),
"peer_fqdn": peer.FQDN(am.GetDNSDomain())},
})
if err != nil {
return err
}
}
for _, p := range removedPeers {
peer := account.Peers[p]
if peer == nil {
log.Errorf("peer %s not found under account %s while saving group", p, accountID)
continue
}
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupRemovedFromPeer,
InitiatorID: userID,
TargetID: peer.IP.String(),
AccountID: accountID,
Meta: map[string]any{"group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(),
"peer_fqdn": peer.FQDN(am.GetDNSDomain())},
})
if err != nil {
return err
}
}
return am.updateAccountPeers(account)
}
// difference returns the elements in `a` that aren't in `b`.
func difference(a, b []string) []string {
mb := make(map[string]struct{}, len(b))
for _, x := range b {
mb[x] = struct{}{}
}
var diff []string
for _, x := range a {
if _, found := mb[x]; !found {
diff = append(diff, x)
}
}
return diff
}
// UpdateGroup updates a group using a list of operations
func (am *DefaultAccountManager) UpdateGroup(accountID string,
groupID string, operations []GroupUpdateOperation) (*Group, error) {

View File

@ -435,10 +435,7 @@ func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *prot
func toPeerConfig(peer *Peer, network *Network, dnsName string) *proto.PeerConfig {
netmask, _ := network.Net.Mask.Size()
fqdn := ""
if dnsName != "" {
fqdn = peer.DNSLabel + "." + dnsName
}
fqdn := peer.FQDN(dnsName)
return &proto.PeerConfig{
Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask), // take it from the network
SshConfig: &proto.SSHConfig{SshEnabled: peer.SSHEnabled},
@ -449,10 +446,7 @@ func toPeerConfig(peer *Peer, network *Network, dnsName string) *proto.PeerConfi
func toRemotePeerConfig(peers []*Peer, dnsName string) []*proto.RemotePeerConfig {
remotePeers := []*proto.RemotePeerConfig{}
for _, rPeer := range peers {
fqdn := ""
if dnsName != "" {
fqdn = rPeer.DNSLabel + "." + dnsName
}
fqdn := rPeer.FQDN(dnsName)
remotePeers = append(remotePeers, &proto.RemotePeerConfig{
WgPubKey: rPeer.Key,
AllowedIps: []string{fmt.Sprintf(AllowedIPsFormat, rPeer.IP)},

View File

@ -18,6 +18,8 @@ tags:
description: Interact with and view information about routes.
- name: DNS
description: Interact with and view information about DNS configuration.
- name: Events
description: View information about the account and network events.
components:
schemas:
User:
@ -45,12 +47,12 @@ components:
items:
type: string
required:
- id
- email
- name
- role
- auto_groups
- status
- id
- email
- name
- role
- auto_groups
- status
UserRequest:
type: object
properties:
@ -96,8 +98,8 @@ components:
description: Peer's hostname
type: string
required:
- id
- name
- id
- name
Peer:
allOf:
- $ref: '#/components/schemas/PeerMinimum'
@ -140,15 +142,15 @@ components:
description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
type: string
required:
- ip
- connected
- last_seen
- os
- version
- groups
- ssh_enabled
- hostname
- dns_label
- ip
- connected
- last_seen
- os
- version
- groups
- ssh_enabled
- hostname
- dns_label
SetupKey:
type: object
properties:
@ -197,19 +199,19 @@ components:
description: A number of times this key can be used. The value of 0 indicates the unlimited usage.
type: integer
required:
- id
- key
- name
- expires
- type
- valid
- revoked
- used_times
- last_used
- state
- auto_groups
- updated_at
- usage_limit
- id
- key
- name
- expires
- type
- valid
- revoked
- used_times
- last_used
- state
- auto_groups
- updated_at
- usage_limit
SetupKeyRequest:
type: object
properties:
@ -253,9 +255,9 @@ components:
description: Count of peers associated to the group
type: integer
required:
- id
- name
- peers_count
- id
- name
- peers_count
Group:
allOf:
- $ref: '#/components/schemas/GroupMinimum'
@ -267,7 +269,7 @@ components:
items:
$ref: '#/components/schemas/PeerMinimum'
required:
- peers
- peers
PatchMinimum:
type: object
properties:
@ -311,10 +313,10 @@ components:
description: Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
type: string
required:
- name
- description
- disabled
- flow
- name
- description
- disabled
- flow
Rule:
allOf:
- type: object
@ -323,7 +325,7 @@ components:
description: Rule ID
type: string
required:
- id
- id
- $ref: '#/components/schemas/RuleMinimum'
- type: object
properties:
@ -338,8 +340,8 @@ components:
items:
$ref: '#/components/schemas/GroupMinimum'
required:
- sources
- destinations
- sources
- destinations
RulePatchOperation:
allOf:
- $ref: '#/components/schemas/PatchMinimum'
@ -428,7 +430,7 @@ components:
ns_type:
description: Nameserver Type
type: string
enum: ["udp"]
enum: [ "udp" ]
port:
description: Nameserver Port
type: integer
@ -498,32 +500,74 @@ components:
path:
description: Nameserver group field to update in form /<field>
type: string
enum: [ "name", "description", "enabled", "groups", "nameservers", "primary", "domains" ]
enum: [ "name", "description", "enabled", "groups", "nameservers", "primary", "domains" ]
required:
- path
Event:
type: object
properties:
id:
description: Event unique identifier
type: string
timestamp:
description: The date and time when the event occurred
type: string
format: date-time
activity:
description: The activity that occurred during the event
type: string
activity_code:
description: The string code of the activity that occurred during the event
type: string
enum: [ "user.peer.delete", "user.join", "user.invite", "user.peer.add", "user.group.add", "user.group.delete",
"user.role.update",
"setupkey.peer.add", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse",
"setupkey.group.delete", "setupkey.group.add"
"rule.add", "rule.delete", "rule.update",
"group.add", "group.update",
"account.create",
]
initiator_id:
description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
type: string
target_id:
description: The ID of the target of the event. E.g., an ID of the peer that a user removed.
type: string
meta:
description: The metadata of the event
type: object
additionalProperties:
type: string
required:
- id
- timestamp
- activity
- activity_code
- initiator_id
- target_id
- meta
responses:
not_found:
description: Resource not found
content: {}
content: { }
validation_failed_simple:
description: Validation failed
content: {}
content: { }
bad_request:
description: Bad Request
content: {}
content: { }
internal_error:
description: Internal Server Error
content: { }
validation_failed:
description: Validation failed
content: {}
content: { }
forbidden:
description: Forbidden
content: {}
content: { }
requires_authentication:
description: Requires authentication
content: {}
content: { }
securitySchemes:
BearerAuth:
type: http
@ -535,9 +579,9 @@ paths:
/api/users:
get:
summary: Returns a list of all users
tags: [Users]
tags: [ Users ]
security:
- BearerAuth: []
- BearerAuth: [ ]
responses:
'200':
description: A JSON array of Users
@ -558,7 +602,7 @@ paths:
/api/users/:
post:
summary: Create a User (invite)
tags: [ Users]
tags: [ Users ]
security:
- BearerAuth: [ ]
requestBody:
@ -585,7 +629,7 @@ paths:
/api/users/{id}:
put:
summary: Update information about a User
tags: [ Users]
tags: [ Users ]
security:
- BearerAuth: [ ]
parameters:
@ -619,9 +663,9 @@ paths:
/api/peers:
get:
summary: Returns a list of all peers
tags: [Peers]
tags: [ Peers ]
security:
- BearerAuth: []
- BearerAuth: [ ]
responses:
'200':
description: A JSON Array of Peers
@ -642,7 +686,7 @@ paths:
/api/peers/{id}:
get:
summary: Get information about a peer
tags: [Peers]
tags: [ Peers ]
security:
- BearerAuth: [ ]
parameters:
@ -669,7 +713,7 @@ paths:
"$ref": "#/components/responses/internal_error"
put:
summary: Update information about a peer
tags: [Peers]
tags: [ Peers ]
security:
- BearerAuth: [ ]
parameters:
@ -710,7 +754,7 @@ paths:
"$ref": "#/components/responses/internal_error"
delete:
summary: Delete a peer
tags: [Peers]
tags: [ Peers ]
security:
- BearerAuth: [ ]
parameters:
@ -723,7 +767,7 @@ paths:
responses:
'200':
description: Delete status code
content: {}
content: { }
'400':
"$ref": "#/components/responses/bad_request"
'401':
@ -735,7 +779,7 @@ paths:
/api/setup-keys:
get:
summary: Returns a list of all Setup Keys
tags: [Setup Keys]
tags: [ Setup Keys ]
security:
- BearerAuth: [ ]
responses:
@ -757,7 +801,7 @@ paths:
"$ref": "#/components/responses/internal_error"
post:
summary: Creates a Setup Key
tags: [Setup Keys]
tags: [ Setup Keys ]
security:
- BearerAuth: [ ]
requestBody:
@ -784,7 +828,7 @@ paths:
/api/setup-keys/{id}:
get:
summary: Get information about a Setup Key
tags: [Setup Keys]
tags: [ Setup Keys ]
security:
- BearerAuth: [ ]
parameters:
@ -811,7 +855,7 @@ paths:
"$ref": "#/components/responses/internal_error"
put:
summary: Update information about a Setup Key
tags: [Setup Keys]
tags: [ Setup Keys ]
security:
- BearerAuth: [ ]
parameters:
@ -844,7 +888,7 @@ paths:
"$ref": "#/components/responses/internal_error"
delete:
summary: Delete a Setup Key
tags: [Setup Keys]
tags: [ Setup Keys ]
security:
- BearerAuth: [ ]
parameters:
@ -857,7 +901,7 @@ paths:
responses:
'200':
description: Delete status code
content: {}
content: { }
'400':
"$ref": "#/components/responses/bad_request"
'401':
@ -869,7 +913,7 @@ paths:
/api/groups:
get:
summary: Returns a list of all Groups
tags: [Groups]
tags: [ Groups ]
security:
- BearerAuth: [ ]
responses:
@ -891,7 +935,7 @@ paths:
"$ref": "#/components/responses/internal_error"
post:
summary: Creates a Group
tags: [Groups]
tags: [ Groups ]
security:
- BearerAuth: [ ]
requestBody:
@ -927,7 +971,7 @@ paths:
/api/groups/{id}:
get:
summary: Get information about a Group
tags: [Groups]
tags: [ Groups ]
security:
- BearerAuth: [ ]
parameters:
@ -954,7 +998,7 @@ paths:
"$ref": "#/components/responses/internal_error"
put:
summary: Update/Replace a Group
tags: [Groups]
tags: [ Groups ]
security:
- BearerAuth: [ ]
parameters:
@ -1029,7 +1073,7 @@ paths:
"$ref": "#/components/responses/internal_error"
delete:
summary: Delete a Group
tags: [Groups]
tags: [ Groups ]
security:
- BearerAuth: [ ]
parameters:
@ -1042,7 +1086,7 @@ paths:
responses:
'200':
description: Delete status code
content: {}
content: { }
'400':
"$ref": "#/components/responses/bad_request"
'401':
@ -1054,7 +1098,7 @@ paths:
/api/rules:
get:
summary: Returns a list of all Rules
tags: [Rules]
tags: [ Rules ]
security:
- BearerAuth: [ ]
responses:
@ -1076,7 +1120,7 @@ paths:
"$ref": "#/components/responses/internal_error"
post:
summary: Creates a Rule
tags: [Rules]
tags: [ Rules ]
security:
- BearerAuth: [ ]
requestBody:
@ -1106,7 +1150,7 @@ paths:
/api/rules/{id}:
get:
summary: Get information about a Rules
tags: [Rules]
tags: [ Rules ]
security:
- BearerAuth: [ ]
parameters:
@ -1133,7 +1177,7 @@ paths:
"$ref": "#/components/responses/internal_error"
put:
summary: Update/Replace a Rule
tags: [Rules]
tags: [ Rules ]
security:
- BearerAuth: [ ]
parameters:
@ -1212,7 +1256,7 @@ paths:
"$ref": "#/components/responses/internal_error"
delete:
summary: Delete a Rule
tags: [Rules]
tags: [ Rules ]
security:
- BearerAuth: [ ]
parameters:
@ -1225,7 +1269,7 @@ paths:
responses:
'200':
description: Delete status code
content: {}
content: { }
'400':
"$ref": "#/components/responses/bad_request"
'401':
@ -1573,5 +1617,28 @@ paths:
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
/api/events:
get:
summary: Returns a list of all events
tags: [ Events ]
security:
- BearerAuth: [ ]
responses:
'200':
description: A JSON Array of Events
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Event'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"

View File

@ -11,6 +11,28 @@ const (
BearerAuthScopes = "BearerAuth.Scopes"
)
// Defines values for EventActivityCode.
const (
EventActivityCodeAccountCreate EventActivityCode = "account.create"
EventActivityCodeGroupAdd EventActivityCode = "group.add"
EventActivityCodeGroupUpdate EventActivityCode = "group.update"
EventActivityCodeRuleAdd EventActivityCode = "rule.add"
EventActivityCodeRuleDelete EventActivityCode = "rule.delete"
EventActivityCodeRuleUpdate EventActivityCode = "rule.update"
EventActivityCodeSetupkeyAdd EventActivityCode = "setupkey.add"
EventActivityCodeSetupkeyOveruse EventActivityCode = "setupkey.overuse"
EventActivityCodeSetupkeyPeerAdd EventActivityCode = "setupkey.peer.add"
EventActivityCodeSetupkeyRevoke EventActivityCode = "setupkey.revoke"
EventActivityCodeSetupkeyUpdate EventActivityCode = "setupkey.update"
EventActivityCodeUserGroupAdd EventActivityCode = "user.group.add"
EventActivityCodeUserGroupDelete EventActivityCode = "user.group.delete"
EventActivityCodeUserInvite EventActivityCode = "user.invite"
EventActivityCodeUserJoin EventActivityCode = "user.join"
EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add"
EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete"
EventActivityCodeUserRoleUpdate EventActivityCode = "user.role.update"
)
// Defines values for GroupPatchOperationOp.
const (
GroupPatchOperationOpAdd GroupPatchOperationOp = "add"
@ -97,6 +119,33 @@ const (
UserStatusInvited UserStatus = "invited"
)
// Event defines model for Event.
type Event struct {
// Activity The activity that occurred during the event
Activity string `json:"activity"`
// ActivityCode The string code of the activity that occurred during the event
ActivityCode EventActivityCode `json:"activity_code"`
// Id Event unique identifier
Id string `json:"id"`
// InitiatorId The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
InitiatorId string `json:"initiator_id"`
// Meta The metadata of the event
Meta map[string]string `json:"meta"`
// TargetId The ID of the target of the event. E.g., an ID of the peer that a user removed.
TargetId string `json:"target_id"`
// Timestamp The date and time when the event occurred
Timestamp time.Time `json:"timestamp"`
}
// EventActivityCode The string code of the activity that occurred during the event
type EventActivityCode string
// Group defines model for Group.
type Group struct {
// Id Group ID

View File

@ -0,0 +1,69 @@
package http
import (
"fmt"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/jwtclaims"
log "github.com/sirupsen/logrus"
"net/http"
)
// Events HTTP handler
type Events struct {
accountManager server.AccountManager
authAudience string
jwtExtractor jwtclaims.ClaimsExtractor
}
// NewEvents creates a new Events HTTP handler
func NewEvents(accountManager server.AccountManager, authAudience string) *Events {
return &Events{
accountManager: accountManager,
authAudience: authAudience,
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
}
}
// GetEvents list of the given account
func (h *Events) GetEvents(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
accountEvents, err := h.accountManager.GetEvents(account.Id, user.Id)
if err != nil {
util.WriteError(err, w)
return
}
events := make([]*api.Event, 0)
for _, e := range accountEvents {
events = append(events, toEventResponse(e))
}
util.WriteJSONObject(w, events)
}
func toEventResponse(event *activity.Event) *api.Event {
meta := make(map[string]string)
if event.Meta != nil {
for s, a := range event.Meta {
meta[s] = fmt.Sprintf("%v", a)
}
}
return &api.Event{
Id: fmt.Sprint(event.ID),
InitiatorId: event.InitiatorID,
Activity: event.Activity.Message(),
ActivityCode: api.EventActivityCode(event.Activity.StringCode()),
TargetId: event.TargetID,
Timestamp: event.Timestamp,
Meta: meta,
}
}

View File

@ -0,0 +1,250 @@
package http
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/stretchr/testify/assert"
"io"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
)
func initEventsTestData(account string, user *server.User, events ...*activity.Event) *Events {
return &Events{
accountManager: &mock_server.MockAccountManager{
GetEventsFunc: func(accountID, userID string) ([]*activity.Event, error) {
if accountID == account {
return events, nil
}
return []*activity.Event{}, nil
},
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
return &server.Account{
Id: claims.AccountId,
Domain: "hotmail.com",
Users: map[string]*server.User{
user.Id: user,
},
}, user, nil
},
},
authAudience: "",
jwtExtractor: jwtclaims.ClaimsExtractor{
ExtractClaimsFromRequestContext: func(r *http.Request, authAudiance string) jwtclaims.AuthorizationClaims {
return jwtclaims.AuthorizationClaims{
UserId: "test_user",
Domain: "hotmail.com",
AccountId: "test_account",
}
},
},
}
}
func generateEvents(accountID, userID string) []*activity.Event {
ID := uint64(1)
events := make([]*activity.Event, 0)
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.PeerAddedByUser,
ID: ID,
InitiatorID: userID,
TargetID: "100.64.0.2",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.UserJoined,
ID: ID,
InitiatorID: userID,
TargetID: "",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupCreated,
ID: ID,
InitiatorID: userID,
TargetID: "group-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.SetupKeyUpdated,
ID: ID,
InitiatorID: userID,
TargetID: "setup-key-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.SetupKeyUpdated,
ID: ID,
InitiatorID: userID,
TargetID: "setup-key-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.SetupKeyRevoked,
ID: ID,
InitiatorID: userID,
TargetID: "setup-key-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.SetupKeyOverused,
ID: ID,
InitiatorID: userID,
TargetID: "setup-key-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.SetupKeyCreated,
ID: ID,
InitiatorID: userID,
TargetID: "setup-key-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.RuleAdded,
ID: ID,
InitiatorID: userID,
TargetID: "some-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.RuleRemoved,
ID: ID,
InitiatorID: userID,
TargetID: "some-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.RuleUpdated,
ID: ID,
InitiatorID: userID,
TargetID: "some-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
ID++
events = append(events, &activity.Event{
Timestamp: time.Now(),
Activity: activity.PeerAddedWithSetupKey,
ID: ID,
InitiatorID: userID,
TargetID: "some-id",
AccountID: accountID,
Meta: map[string]any{"some": "meta"},
})
return events
}
func TestEvents_GetEvents(t *testing.T) {
tt := []struct {
name string
expectedStatus int
expectedBody bool
requestType string
requestPath string
requestBody io.Reader
}{
{
name: "GetEvents OK",
expectedBody: true,
requestType: http.MethodGet,
requestPath: "/api/events/",
expectedStatus: http.StatusOK,
},
}
accountID := "test_account"
adminUser := server.NewAdminUser("test_user")
events := generateEvents(accountID, adminUser.Id)
handler := initEventsTestData(accountID, adminUser, events...)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
router := mux.NewRouter()
router.HandleFunc("/api/events/", handler.GetEvents).Methods("GET")
router.ServeHTTP(recorder, req)
res := recorder.Result()
defer res.Body.Close()
if status := recorder.Code; status != tc.expectedStatus {
t.Errorf("handler returned wrong status code: got %v want %v",
status, tc.expectedStatus)
return
}
if !tc.expectedBody {
return
}
content, err := io.ReadAll(res.Body)
if err != nil {
t.Fatalf("I don't know what I expected; %v", err)
}
var got []*api.Event
if err = json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err)
}
assert.Len(t, got, len(events))
actual := map[string]*api.Event{}
for _, event := range got {
actual[event.Id] = event
}
for _, expected := range events {
event, ok := actual[strconv.FormatUint(expected.ID, 10)]
assert.True(t, ok)
assert.Equal(t, expected.InitiatorID, event.InitiatorId)
assert.Equal(t, expected.TargetID, event.TargetId)
assert.Equal(t, expected.Activity.Message(), event.Activity)
assert.Equal(t, expected.Activity.StringCode(), string(event.ActivityCode))
assert.Equal(t, expected.Meta["some"], event.Meta["some"])
assert.True(t, expected.Timestamp.Equal(event.Timestamp))
}
})
}
}

View File

@ -51,7 +51,7 @@ func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
// UpdateGroupHandler handles update to a group identified by a given ID
func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -102,7 +102,7 @@ func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
Peers: peerIPsToKeys(account, req.Peers),
}
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
if err := h.accountManager.SaveGroup(account.Id, user.Id, &group); err != nil {
log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err)
util.WriteError(err, w)
return
@ -219,7 +219,7 @@ func (h *Groups) PatchGroupHandler(w http.ResponseWriter, r *http.Request) {
// CreateGroupHandler handles group creation request
func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -243,7 +243,7 @@ func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) {
Peers: peerIPsToKeys(account, req.Peers),
}
err = h.accountManager.SaveGroup(account.Id, &group)
err = h.accountManager.SaveGroup(account.Id, user.Id, &group)
if err != nil {
util.WriteError(err, w)
return

View File

@ -29,7 +29,7 @@ var TestPeers = map[string]*server.Peer{
func initGroupTestData(user *server.User, groups ...*server.Group) *Groups {
return &Groups{
accountManager: &mock_server.MockAccountManager{
SaveGroupFunc: func(accountID string, group *server.Group) error {
SaveGroupFunc: func(accountID, userID string, group *server.Group) error {
if !strings.HasPrefix(group.ID, "id-") {
group.ID = "id-was-set"
}

View File

@ -40,6 +40,7 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
userHandler := NewUserHandler(accountManager, authAudience)
routesHandler := NewRoutes(accountManager, authAudience)
nameserversHandler := NewNameservers(accountManager, authAudience)
eventsHandler := NewEvents(accountManager, authAudience)
apiHandler.HandleFunc("/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/peers/{id}", peersHandler.HandlePeer).
@ -81,6 +82,8 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
apiHandler.HandleFunc("/dns/nameservers/{id}", nameserversHandler.GetNameserverGroupHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/dns/nameservers/{id}", nameserversHandler.DeleteNameserverGroupHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/events", eventsHandler.GetEvents).Methods("GET", "OPTIONS")
err = apiHandler.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
methods, err := route.GetMethods()
if err != nil {

View File

@ -45,8 +45,8 @@ func (h *Peers) updatePeer(account *server.Account, peer *server.Peer, w http.Re
util.WriteJSONObject(w, toPeerResponse(peer, account, dnsDomain))
}
func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
_, err := h.accountManager.DeletePeer(accountId, peer.Key)
func (h *Peers) deletePeer(accountID, userID string, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
_, err := h.accountManager.DeletePeer(accountID, peer.Key, userID)
if err != nil {
util.WriteError(err, w)
return
@ -56,7 +56,7 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW
func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -78,7 +78,7 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodDelete:
h.deletePeer(account.Id, peer, w, r)
h.deletePeer(account.Id, user.Id, peer, w, r)
return
case http.MethodPut:
h.updatePeer(account, peer, w, r)
@ -143,9 +143,10 @@ func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string
}
}
}
fqdn := peer.DNSLabel
if dnsDomain != "" {
fqdn = peer.DNSLabel + "." + dnsDomain
fqdn := peer.FQDN(dnsDomain)
if fqdn == "" {
fqdn = peer.DNSLabel
}
return &api.Peer{
Id: peer.IP.String(),

View File

@ -52,7 +52,7 @@ func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
// UpdateRuleHandler handles update to a rule identified by a given ID
func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -109,7 +109,7 @@ func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
return
}
err = h.accountManager.SaveRule(account.Id, &rule)
err = h.accountManager.SaveRule(account.Id, user.Id, &rule)
if err != nil {
util.WriteError(err, w)
return
@ -267,7 +267,7 @@ func (h *Rules) PatchRuleHandler(w http.ResponseWriter, r *http.Request) {
// CreateRuleHandler handles rule creation request
func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -312,7 +312,7 @@ func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
return
}
err = h.accountManager.SaveRule(account.Id, &rule)
err = h.accountManager.SaveRule(account.Id, user.Id, &rule)
if err != nil {
util.WriteError(err, w)
return
@ -326,7 +326,7 @@ func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
// DeleteRuleHandler handles rule deletion request
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -339,7 +339,7 @@ func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
return
}
err = h.accountManager.DeleteRule(aID, rID)
err = h.accountManager.DeleteRule(aID, rID, user.Id)
if err != nil {
util.WriteError(err, w)
return

View File

@ -22,7 +22,7 @@ import (
func initRulesTestData(rules ...*server.Rule) *Rules {
return &Rules{
accountManager: &mock_server.MockAccountManager{
SaveRuleFunc: func(_ string, rule *server.Rule) error {
SaveRuleFunc: func(_, _ string, rule *server.Rule) error {
if !strings.HasPrefix(rule.ID, "id-") {
rule.ID = "id-was-set"
}

View File

@ -30,7 +30,7 @@ func NewSetupKeysHandler(accountManager server.AccountManager, authAudience stri
// CreateSetupKeyHandler is a POST requests that creates a new SetupKey
func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -61,7 +61,7 @@ func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request
}
setupKey, err := h.accountManager.CreateSetupKey(account.Id, req.Name, server.SetupKeyType(req.Type), expiresIn,
req.AutoGroups, req.UsageLimit)
req.AutoGroups, req.UsageLimit, user.Id)
if err != nil {
util.WriteError(err, w)
return
@ -98,7 +98,7 @@ func (h *SetupKeys) GetSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
// UpdateSetupKeyHandler is a PUT request to update server.SetupKey
func (h *SetupKeys) UpdateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -134,7 +134,7 @@ func (h *SetupKeys) UpdateSetupKeyHandler(w http.ResponseWriter, r *http.Request
newKey.Name = req.Name
newKey.Id = keyID
newKey, err = h.accountManager.SaveSetupKey(account.Id, newKey)
newKey, err = h.accountManager.SaveSetupKey(account.Id, newKey, user.Id)
if err != nil {
util.WriteError(err, w)
return

View File

@ -47,7 +47,7 @@ func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.Setup
}, user, nil
},
CreateSetupKeyFunc: func(_ string, keyName string, typ server.SetupKeyType, _ time.Duration, _ []string,
_ int) (*server.SetupKey, error) {
_ int, _ string) (*server.SetupKey, error) {
if keyName == newKey.Name || typ != newKey.Type {
return newKey, nil
}
@ -64,7 +64,7 @@ func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.Setup
}
},
SaveSetupKeyFunc: func(accountID string, key *server.SetupKey) (*server.SetupKey, error) {
SaveSetupKeyFunc: func(accountID string, key *server.SetupKey, _ string) (*server.SetupKey, error) {
if key.Id == updatedSetupKey.Id {
return updatedSetupKey, nil
}

View File

@ -34,7 +34,7 @@ func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
}
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -60,7 +60,7 @@ func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
return
}
newUser, err := h.accountManager.SaveUser(account.Id, &server.User{
newUser, err := h.accountManager.SaveUser(account.Id, user.Id, &server.User{
Id: userID,
Role: userRole,
AutoGroups: req.AutoGroups,
@ -81,7 +81,7 @@ func (h *UserHandler) CreateUserHandler(w http.ResponseWriter, r *http.Request)
}
claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience)
account, _, err := h.accountManager.GetAccountFromToken(claims)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
@ -99,7 +99,7 @@ func (h *UserHandler) CreateUserHandler(w http.ResponseWriter, r *http.Request)
return
}
newUser, err := h.accountManager.CreateUser(account.Id, &server.UserInfo{
newUser, err := h.accountManager.CreateUser(account.Id, user.Id, &server.UserInfo{
Email: req.Email,
Name: *req.Name,
Role: req.Role,

View File

@ -3,6 +3,7 @@ package server
import (
"context"
"fmt"
"github.com/netbirdio/netbird/management/server/activity"
"net"
"os"
"path/filepath"
@ -403,7 +404,9 @@ func startManagement(t *testing.T, port int, config *Config) (*grpc.Server, erro
return nil, err
}
peersUpdateManager := NewPeersUpdateManager()
accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "")
eventStore := &activity.InMemoryEventStore{}
accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "",
eventStore)
if err != nil {
return nil, err
}

View File

@ -2,6 +2,7 @@ package server_test
import (
"context"
"github.com/netbirdio/netbird/management/server/activity"
"math/rand"
"net"
"os"
@ -493,7 +494,9 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "")
eventStore := &activity.InMemoryEventStore{}
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore)
if err != nil {
log.Fatalf("failed creating a manager: %v", err)
}

View File

@ -3,6 +3,7 @@ package mock_server
import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/route"
"google.golang.org/grpc/codes"
@ -11,9 +12,10 @@ import (
)
type MockAccountManager struct {
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
GetAccountByUserFunc func(userId string) (*server.Account, error)
CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration, autoGroups []string, usageLimit int) (*server.SetupKey, error)
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
GetAccountByUserFunc func(userId string) (*server.Account, error)
CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType,
expiresIn time.Duration, autoGroups []string, usageLimit int, userID string) (*server.SetupKey, error)
GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error)
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error)
@ -21,13 +23,13 @@ type MockAccountManager struct {
GetPeerFunc func(peerKey string) (*server.Peer, error)
GetPeersFunc func(accountID, userID string) ([]*server.Peer, error)
MarkPeerConnectedFunc func(peerKey string, connected bool) error
DeletePeerFunc func(accountId string, peerKey string) (*server.Peer, error)
DeletePeerFunc func(accountID, peerKey, userID string) (*server.Peer, error)
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, error)
GetGroupFunc func(accountID, groupID string) (*server.Group, error)
SaveGroupFunc func(accountID string, group *server.Group) error
SaveGroupFunc func(accountID, userID string, group *server.Group) error
UpdateGroupFunc func(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error)
DeleteGroupFunc func(accountID, groupID string) error
ListGroupsFunc func(accountID string) ([]*server.Group, error)
@ -35,9 +37,9 @@ type MockAccountManager struct {
GroupDeletePeerFunc func(accountID, groupID, peerKey string) error
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error)
SaveRuleFunc func(accountID string, rule *server.Rule) error
SaveRuleFunc func(accountID, userID string, rule *server.Rule) error
UpdateRuleFunc func(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error)
DeleteRuleFunc func(accountID, ruleID string) error
DeleteRuleFunc func(accountID, ruleID, userID string) error
ListRulesFunc func(accountID, userID string) ([]*server.Rule, error)
GetUsersFromAccountFunc func(accountID, userID string) ([]*server.UserInfo, error)
UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) error
@ -49,18 +51,19 @@ type MockAccountManager struct {
UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error)
DeleteRouteFunc func(accountID, routeID string) error
ListRoutesFunc func(accountID, userID string) ([]*route.Route, error)
SaveSetupKeyFunc func(accountID string, key *server.SetupKey) (*server.SetupKey, error)
SaveSetupKeyFunc func(accountID string, key *server.SetupKey, userID string) (*server.SetupKey, error)
ListSetupKeysFunc func(accountID, userID string) ([]*server.SetupKey, error)
SaveUserFunc func(accountID string, user *server.User) (*server.UserInfo, error)
SaveUserFunc func(accountID, userID string, user *server.User) (*server.UserInfo, error)
GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool) (*nbdns.NameServerGroup, error)
SaveNameServerGroupFunc func(accountID string, nsGroupToSave *nbdns.NameServerGroup) error
UpdateNameServerGroupFunc func(accountID, nsGroupID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error)
DeleteNameServerGroupFunc func(accountID, nsGroupID string) error
ListNameServerGroupsFunc func(accountID string) ([]*nbdns.NameServerGroup, error)
CreateUserFunc func(accountID string, key *server.UserInfo) (*server.UserInfo, error)
CreateUserFunc func(accountID, userID string, key *server.UserInfo) (*server.UserInfo, error)
GetAccountFromTokenFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error)
GetDNSDomainFunc func() string
GetEventsFunc func(accountID, userID string) ([]*activity.Event, error)
}
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
@ -72,9 +75,9 @@ func (am *MockAccountManager) GetUsersFromAccount(accountID string, userID strin
}
// DeletePeer mock implementation of DeletePeer from server.AccountManager interface
func (am *MockAccountManager) DeletePeer(accountId string, peerKey string) (*server.Peer, error) {
func (am *MockAccountManager) DeletePeer(accountID, peerKey, userID string) (*server.Peer, error) {
if am.DeletePeerFunc != nil {
return am.DeletePeerFunc(accountId, peerKey)
return am.DeletePeerFunc(accountID, peerKey, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method DeletePeer is not implemented")
}
@ -108,9 +111,10 @@ func (am *MockAccountManager) CreateSetupKey(
expiresIn time.Duration,
autoGroups []string,
usageLimit int,
userID string,
) (*server.SetupKey, error) {
if am.CreateSetupKeyFunc != nil {
return am.CreateSetupKeyFunc(accountID, keyName, keyType, expiresIn, autoGroups, usageLimit)
return am.CreateSetupKeyFunc(accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented")
}
@ -197,9 +201,9 @@ func (am *MockAccountManager) GetGroup(accountID, groupID string) (*server.Group
}
// SaveGroup mock implementation of SaveGroup from server.AccountManager interface
func (am *MockAccountManager) SaveGroup(accountID string, group *server.Group) error {
func (am *MockAccountManager) SaveGroup(accountID, userID string, group *server.Group) error {
if am.SaveGroupFunc != nil {
return am.SaveGroupFunc(accountID, group)
return am.SaveGroupFunc(accountID, userID, group)
}
return status.Errorf(codes.Unimplemented, "method SaveGroup is not implemented")
}
@ -261,9 +265,9 @@ func (am *MockAccountManager) GetRule(accountID, ruleID, userID string) (*server
}
// SaveRule mock implementation of SaveRule from server.AccountManager interface
func (am *MockAccountManager) SaveRule(accountID string, rule *server.Rule) error {
func (am *MockAccountManager) SaveRule(accountID, userID string, rule *server.Rule) error {
if am.SaveRuleFunc != nil {
return am.SaveRuleFunc(accountID, rule)
return am.SaveRuleFunc(accountID, userID, rule)
}
return status.Errorf(codes.Unimplemented, "method SaveRule is not implemented")
}
@ -277,9 +281,9 @@ func (am *MockAccountManager) UpdateRule(accountID string, ruleID string, operat
}
// DeleteRule mock implementation of DeleteRule from server.AccountManager interface
func (am *MockAccountManager) DeleteRule(accountID, ruleID string) error {
func (am *MockAccountManager) DeleteRule(accountID, ruleID, userID string) error {
if am.DeleteRuleFunc != nil {
return am.DeleteRuleFunc(accountID, ruleID)
return am.DeleteRuleFunc(accountID, ruleID, userID)
}
return status.Errorf(codes.Unimplemented, "method DeleteRule is not implemented")
}
@ -373,9 +377,9 @@ func (am *MockAccountManager) ListRoutes(accountID, userID string) ([]*route.Rou
}
// SaveSetupKey mocks SaveSetupKey of the AccountManager interface
func (am *MockAccountManager) SaveSetupKey(accountID string, key *server.SetupKey) (*server.SetupKey, error) {
func (am *MockAccountManager) SaveSetupKey(accountID string, key *server.SetupKey, userID string) (*server.SetupKey, error) {
if am.SaveSetupKeyFunc != nil {
return am.SaveSetupKeyFunc(accountID, key)
return am.SaveSetupKeyFunc(accountID, key, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method SaveSetupKey is not implemented")
@ -400,9 +404,9 @@ func (am *MockAccountManager) ListSetupKeys(accountID, userID string) ([]*server
}
// SaveUser mocks SaveUser of the AccountManager interface
func (am *MockAccountManager) SaveUser(accountID string, user *server.User) (*server.UserInfo, error) {
func (am *MockAccountManager) SaveUser(accountID, userID string, user *server.User) (*server.UserInfo, error) {
if am.SaveUserFunc != nil {
return am.SaveUserFunc(accountID, user)
return am.SaveUserFunc(accountID, userID, user)
}
return nil, status.Errorf(codes.Unimplemented, "method SaveUser is not implemented")
}
@ -456,9 +460,9 @@ func (am *MockAccountManager) ListNameServerGroups(accountID string) ([]*nbdns.N
}
// CreateUser mocks CreateUser of the AccountManager interface
func (am *MockAccountManager) CreateUser(accountID string, invite *server.UserInfo) (*server.UserInfo, error) {
func (am *MockAccountManager) CreateUser(accountID, userID string, invite *server.UserInfo) (*server.UserInfo, error) {
if am.CreateUserFunc != nil {
return am.CreateUserFunc(accountID, invite)
return am.CreateUserFunc(accountID, userID, invite)
}
return nil, status.Errorf(codes.Unimplemented, "method CreateUser is not implemented")
}
@ -477,7 +481,7 @@ func (am *MockAccountManager) GetPeers(accountID, userID string) ([]*server.Peer
if am.GetAccountFromTokenFunc != nil {
return am.GetPeersFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetPeersFunc is not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetPeers is not implemented")
}
// GetDNSDomain mocks GetDNSDomain of the AccountManager interface
@ -487,3 +491,11 @@ func (am *MockAccountManager) GetDNSDomain() string {
}
return ""
}
// GetEvents mocks GetEvents of the AccountManager interface
func (am *MockAccountManager) GetEvents(accountID, userID string) ([]*activity.Event, error) {
if am.GetEventsFunc != nil {
return am.GetEventsFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetEvents is not implemented")
}

View File

@ -2,6 +2,7 @@ package server
import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/stretchr/testify/require"
"net/netip"
"testing"
@ -1056,7 +1057,8 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
if err != nil {
return nil, err
}
return BuildManager(store, NewPeersUpdateManager(), nil, "", "")
eventStore := &activity.InMemoryEventStore{}
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore)
}
func createNSStore(t *testing.T) (Store, error) {

View File

@ -1,7 +1,9 @@
package server
import (
"fmt"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/status"
"net"
"strings"
@ -73,6 +75,19 @@ func (p *Peer) Copy() *Peer {
}
}
// FQDN returns peers FQDN combined of the peer's DNS label and the system's DNS domain
func (p *Peer) FQDN(dnsDomain string) string {
if dnsDomain == "" {
return ""
}
return fmt.Sprintf("%s.%s", p.DNSLabel, dnsDomain)
}
// EventMeta returns activity event meta related to the peer
func (p *Peer) EventMeta(dnsDomain string) map[string]any {
return map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP}
}
// Copy PeerStatus
func (p *PeerStatus) Copy() *PeerStatus {
return &PeerStatus{
@ -216,7 +231,7 @@ func (am *DefaultAccountManager) UpdatePeer(accountID string, update *Peer) (*Pe
}
// DeletePeer removes peer from the account by its IP
func (am *DefaultAccountManager) DeletePeer(accountID string, peerPubKey string) (*Peer, error) {
func (am *DefaultAccountManager) DeletePeer(accountID, peerPubKey, userID string) (*Peer, error) {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -262,6 +277,18 @@ func (am *DefaultAccountManager) DeletePeer(accountID string, peerPubKey string)
}
am.peersUpdateManager.CloseChannel(peerPubKey)
event := &activity.Event{
Timestamp: time.Now(),
AccountID: account.Id,
InitiatorID: userID,
TargetID: peer.IP.String(),
Activity: activity.PeerRemovedByUser,
Meta: peer.EventMeta(am.GetDNSDomain()),
}
_, err = am.eventStore.Save(event)
if err != nil {
return nil, err
}
return peer, nil
}
@ -359,6 +386,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
return nil, err
}
opEvent := &activity.Event{
Timestamp: time.Now(),
AccountID: account.Id,
}
if !addedByUser {
// validate the setup key if adding with a key
sk, err := account.FindSetupKey(upperKey)
@ -371,6 +403,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
}
account.SetupKeys[sk.Key] = sk.IncrementUsage()
opEvent.InitiatorID = sk.Id
opEvent.Activity = activity.PeerAddedWithSetupKey
} else {
opEvent.InitiatorID = userID
opEvent.Activity = activity.PeerAddedByUser
}
takenIps := account.getTakenIPs()
@ -436,6 +473,13 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
return nil, err
}
opEvent.TargetID = newPeer.IP.String()
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
_, err = am.eventStore.Save(opEvent)
if err != nil {
return nil, err
}
return newPeer, nil
}

View File

@ -87,9 +87,9 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
return
}
expectedId := "test_account"
userId := "account_creator"
account, err := createAccount(manager, expectedId, userId, "")
expectedID := "test_account"
userID := "account_creator"
account, err := createAccount(manager, expectedID, userID, "")
if err != nil {
t.Fatal(err)
}
@ -134,13 +134,13 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
return
}
rules, err := manager.ListRules(account.Id, userId)
rules, err := manager.ListRules(account.Id, userID)
if err != nil {
t.Errorf("expecting to get a list of rules, got failure %v", err)
return
}
err = manager.DeleteRule(account.Id, rules[0].ID)
err = manager.DeleteRule(account.Id, rules[0].ID, userID)
if err != nil {
t.Errorf("expecting to delete 1 group, got failure %v", err)
return
@ -159,12 +159,12 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
group1.Peers = append(group1.Peers, peerKey1.PublicKey().String())
group2.Peers = append(group2.Peers, peerKey2.PublicKey().String())
err = manager.SaveGroup(account.Id, &group1)
err = manager.SaveGroup(account.Id, userID, &group1)
if err != nil {
t.Errorf("expecting group1 to be added, got failure %v", err)
return
}
err = manager.SaveGroup(account.Id, &group2)
err = manager.SaveGroup(account.Id, userID, &group2)
if err != nil {
t.Errorf("expecting group2 to be added, got failure %v", err)
return
@ -174,7 +174,7 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
rule.Source = append(rule.Source, group1.ID)
rule.Destination = append(rule.Destination, group2.ID)
rule.Flow = TrafficFlowBidirect
err = manager.SaveRule(account.Id, &rule)
err = manager.SaveRule(account.Id, userID, &rule)
if err != nil {
t.Errorf("expecting rule to be added, got failure %v", err)
return
@ -222,7 +222,7 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
}
rule.Disabled = true
err = manager.SaveRule(account.Id, &rule)
err = manager.SaveRule(account.Id, userID, &rule)
if err != nil {
t.Errorf("expecting rule to be added, got failure %v", err)
return

View File

@ -1,6 +1,7 @@
package server
import (
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/route"
"github.com/rs/xid"
"github.com/stretchr/testify/require"
@ -14,6 +15,7 @@ const (
routeGroup1 = "routeGroup1"
routeGroup2 = "routeGroup2"
routeInvalidGroup1 = "routeInvalidGroup1"
userID = "testingUser"
)
func TestCreateRoute(t *testing.T) {
@ -831,7 +833,6 @@ func TestDeleteRoute(t *testing.T) {
func TestGetNetworkMap_RouteSync(t *testing.T) {
// no routes for peer in different groups
// no routes when route is deleted
baseRoute := &route.Route{
ID: "testingRoute",
Network: netip.MustParsePrefix("192.168.0.0/16"),
@ -895,7 +896,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
Name: "peer1 group",
Peers: []string{peer1Key},
}
err = am.SaveGroup(account.Id, newGroup)
err = am.SaveGroup(account.Id, userID, newGroup)
require.NoError(t, err)
rules, err := am.ListRules(account.Id, "testingUser")
@ -908,10 +909,10 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
newRule.Source = []string{newGroup.ID}
newRule.Destination = []string{newGroup.ID}
err = am.SaveRule(account.Id, newRule)
err = am.SaveRule(account.Id, userID, newRule)
require.NoError(t, err)
err = am.DeleteRule(account.Id, defaultRule.ID)
err = am.DeleteRule(account.Id, defaultRule.ID, userID)
require.NoError(t, err)
peer1GroupRoutes, err := am.GetNetworkMap(peer1Key)
@ -936,7 +937,8 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) {
if err != nil {
return nil, err
}
return BuildManager(store, NewPeersUpdateManager(), nil, "", "")
eventStore := &activity.InMemoryEventStore{}
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore)
}
func createRouterStore(t *testing.T) (Store, error) {
@ -980,7 +982,6 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
}
accountID := "testingAcc"
userID := "testingUser"
domain := "example.com"
account := newAccountWithId(accountID, userID, domain)
@ -1002,7 +1003,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
Name: routeGroup1,
Peers: []string{peer1Key},
}
err = am.SaveGroup(accountID, newGroup)
err = am.SaveGroup(accountID, userID, newGroup)
if err != nil {
return nil, err
}
@ -1013,7 +1014,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
Peers: []string{peer1Key},
}
err = am.SaveGroup(accountID, newGroup)
err = am.SaveGroup(accountID, userID, newGroup)
if err != nil {
return nil, err
}

View File

@ -1,8 +1,10 @@
package server
import (
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/status"
"strings"
"time"
)
// TrafficFlowType defines allowed direction of the traffic in the rule
@ -87,6 +89,11 @@ func (r *Rule) Copy() *Rule {
}
}
// EventMeta returns activity event meta related to this rule
func (r *Rule) EventMeta() map[string]any {
return map[string]any{"name": r.Name}
}
// GetRule of ACL from the store
func (am *DefaultAccountManager) GetRule(accountID, ruleID, userID string) (*Rule, error) {
unlock := am.Store.AcquireAccountLock(accountID)
@ -115,7 +122,7 @@ func (am *DefaultAccountManager) GetRule(accountID, ruleID, userID string) (*Rul
}
// SaveRule of ACL in the store
func (am *DefaultAccountManager) SaveRule(accountID string, rule *Rule) error {
func (am *DefaultAccountManager) SaveRule(accountID, userID string, rule *Rule) error {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -124,6 +131,8 @@ func (am *DefaultAccountManager) SaveRule(accountID string, rule *Rule) error {
return err
}
_, exists := account.Rules[rule.ID]
account.Rules[rule.ID] = rule
account.Network.IncSerial()
@ -131,6 +140,24 @@ func (am *DefaultAccountManager) SaveRule(accountID string, rule *Rule) error {
return err
}
action := activity.RuleAdded
if exists {
action = activity.RuleUpdated
}
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: action,
InitiatorID: userID,
TargetID: rule.ID,
AccountID: accountID,
Meta: rule.EventMeta(),
})
if err != nil {
return err
}
return am.updateAccountPeers(account)
}
@ -210,7 +237,7 @@ func (am *DefaultAccountManager) UpdateRule(accountID string, ruleID string,
}
// DeleteRule of ACL from the store
func (am *DefaultAccountManager) DeleteRule(accountID, ruleID string) error {
func (am *DefaultAccountManager) DeleteRule(accountID, ruleID, userID string) error {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -219,6 +246,10 @@ func (am *DefaultAccountManager) DeleteRule(accountID, ruleID string) error {
return err
}
rule := account.Rules[ruleID]
if rule == nil {
return status.Errorf(status.NotFound, "rule with ID %s doesn't exist", ruleID)
}
delete(account.Rules, ruleID)
account.Network.IncSerial()
@ -226,6 +257,19 @@ func (am *DefaultAccountManager) DeleteRule(accountID, ruleID string) error {
return err
}
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.RuleRemoved,
InitiatorID: userID,
TargetID: ruleID,
AccountID: accountID,
Meta: rule.EventMeta(),
})
if err != nil {
return err
}
return am.updateAccountPeers(account)
}

View File

@ -2,7 +2,9 @@ package server
import (
"github.com/google/uuid"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/status"
log "github.com/sirupsen/logrus"
"hash/fnv"
"strconv"
"strings"
@ -107,12 +109,20 @@ func (key *SetupKey) Copy() *SetupKey {
}
}
// EventMeta returns activity event meta related to the setup key
func (key *SetupKey) EventMeta() map[string]any {
return map[string]any{"name": key.Name, "type": key.Type, "key": key.HiddenCopy(1).Key}
}
// HiddenCopy returns a copy of the key with a Key value hidden with "*" and a 5 character prefix.
// E.g., "831F6*******************************"
func (key *SetupKey) HiddenCopy() *SetupKey {
func (key *SetupKey) HiddenCopy(length int) *SetupKey {
k := key.Copy()
prefix := k.Key[0:5]
k.Key = prefix + strings.Repeat("*", utf8.RuneCountInString(key.Key)-len(prefix))
if length > utf8.RuneCountInString(key.Key) {
length = utf8.RuneCountInString(key.Key) - len(prefix)
}
k.Key = prefix + strings.Repeat("*", length)
return k
}
@ -189,7 +199,7 @@ func Hash(s string) uint32 {
// CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key,
// and adds it to the specified account. A list of autoGroups IDs can be empty.
func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string, keyType SetupKeyType,
expiresIn time.Duration, autoGroups []string, usageLimit int) (*SetupKey, error) {
expiresIn time.Duration, autoGroups []string, usageLimit int, userID string) (*SetupKey, error) {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -211,12 +221,44 @@ func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string
setupKey := GenerateSetupKey(keyName, keyType, keyDuration, autoGroups, usageLimit)
account.SetupKeys[setupKey.Key] = setupKey
err = am.Store.SaveAccount(account)
if err != nil {
return nil, status.Errorf(status.Internal, "failed adding account key")
}
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.SetupKeyCreated,
InitiatorID: userID,
TargetID: setupKey.Id,
AccountID: accountID,
Meta: setupKey.EventMeta(),
})
if err != nil {
return nil, err
}
for _, g := range setupKey.AutoGroups {
group := account.GetGroup(g)
if group != nil {
_, err := am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupAddedToSetupKey,
InitiatorID: userID,
TargetID: setupKey.Id,
AccountID: accountID,
Meta: map[string]any{"group": group.Name, "group_id": group.ID, "setupkey": setupKey.Name},
})
if err != nil {
log.Errorf("failed saving setup key activity event %s: %v",
activity.GroupAddedToSetupKey.StringCode(), err)
continue
}
} else {
log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id)
}
}
return setupKey, nil
}
@ -224,7 +266,7 @@ func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string
// Due to the unique nature of a SetupKey certain properties must not be overwritten
// (e.g. the key itself, creation date, ID, etc).
// These properties are overwritten: Name, AutoGroups, Revoked. The rest is copied from the existing key.
func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *SetupKey) (*SetupKey, error) {
func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *SetupKey, userID string) (*SetupKey, error) {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -261,6 +303,67 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup
return nil, err
}
if !oldKey.Revoked && newKey.Revoked {
_, err = am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.SetupKeyRevoked,
InitiatorID: userID,
TargetID: newKey.Id,
AccountID: accountID,
Meta: newKey.EventMeta(),
})
if err != nil {
return nil, err
}
}
defer func() {
addedGroups := difference(newKey.AutoGroups, oldKey.AutoGroups)
removedGroups := difference(oldKey.AutoGroups, newKey.AutoGroups)
for _, g := range removedGroups {
group := account.GetGroup(g)
if group != nil {
_, err := am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupRemovedFromSetupKey,
InitiatorID: userID,
TargetID: oldKey.Id,
AccountID: accountID,
Meta: map[string]any{"group": group.Name, "group_id": group.ID, "setupkey": newKey.Name},
})
if err != nil {
log.Errorf("failed saving setup key activity event %s: %v",
activity.GroupRemovedFromSetupKey.StringCode(), err)
continue
}
} else {
log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id)
}
}
for _, g := range addedGroups {
group := account.GetGroup(g)
if group != nil {
_, err := am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupAddedToSetupKey,
InitiatorID: userID,
TargetID: oldKey.Id,
AccountID: accountID,
Meta: map[string]any{"group": group.Name, "group_id": group.ID, "setupkey": newKey.Name},
})
if err != nil {
log.Errorf("failed saving setup key activity event %s: %v",
activity.GroupAddedToSetupKey.StringCode(), err)
continue
}
} else {
log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id)
}
}
}()
return newKey, am.updateAccountPeers(account)
}
@ -282,7 +385,7 @@ func (am *DefaultAccountManager) ListSetupKeys(accountID, userID string) ([]*Set
for _, key := range account.SetupKeys {
var k *SetupKey
if !user.IsAdmin() {
k = key.HiddenCopy()
k = key.HiddenCopy(999)
} else {
k = key.Copy()
}
@ -324,7 +427,7 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, userID, keyID string) (*
}
if !user.IsAdmin() {
foundKey = foundKey.HiddenCopy()
foundKey = foundKey.HiddenCopy(999)
}
return foundKey, nil

View File

@ -1,7 +1,9 @@
package server
import (
"fmt"
"github.com/google/uuid"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/stretchr/testify/assert"
"strconv"
"testing"
@ -20,7 +22,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
t.Fatal(err)
}
err = manager.SaveGroup(account.Id, &Group{
err = manager.SaveGroup(account.Id, userID, &Group{
ID: "group_1",
Name: "group_name_1",
Peers: []string{},
@ -33,7 +35,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
keyName := "my-test-key"
key, err := manager.CreateSetupKey(account.Id, keyName, SetupKeyReusable, expiresIn, []string{},
SetupKeyUnlimitedUsage)
SetupKeyUnlimitedUsage, userID)
if err != nil {
t.Fatal(err)
}
@ -46,13 +48,33 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
Name: newKeyName,
Revoked: revoked,
AutoGroups: autoGroups,
})
}, userID)
if err != nil {
t.Fatal(err)
}
assertKey(t, newKey, newKeyName, revoked, "reusable", 0, key.CreatedAt, key.ExpiresAt,
key.Id, time.Now(), autoGroups)
events, err := manager.GetEvents(account.Id, userID)
if err != nil {
return
}
var ev *activity.Event
for _, event := range events {
if event.Activity == activity.SetupKeyRevoked {
ev = event
}
}
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, newKeyName, ev.Meta["name"])
assert.Equal(t, fmt.Sprint(key.Type), fmt.Sprint(ev.Meta["type"]))
assert.NotEmpty(t, ev.Meta["key"])
assert.Equal(t, userID, ev.InitiatorID)
assert.Equal(t, key.Id, ev.TargetID)
}
func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
@ -67,7 +89,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
t.Fatal(err)
}
err = manager.SaveGroup(account.Id, &Group{
err = manager.SaveGroup(account.Id, userID, &Group{
ID: "group_1",
Name: "group_name_1",
Peers: []string{},
@ -76,7 +98,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
t.Fatal(err)
}
err = manager.SaveGroup(account.Id, &Group{
err = manager.SaveGroup(account.Id, userID, &Group{
ID: "group_2",
Name: "group_name_2",
Peers: []string{},
@ -121,7 +143,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
for _, tCase := range []testCase{testCase1, testCase2} {
t.Run(tCase.name, func(t *testing.T) {
key, err := manager.CreateSetupKey(account.Id, tCase.expectedKeyName, SetupKeyReusable, expiresIn,
tCase.expectedGroups, SetupKeyUnlimitedUsage)
tCase.expectedGroups, SetupKeyUnlimitedUsage, userID)
if tCase.expectedFailure {
if err == nil {
@ -137,6 +159,24 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
assertKey(t, key, tCase.expectedKeyName, false, tCase.expectedType, tCase.expectedUsedTimes,
tCase.expectedCreatedAt, tCase.expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))),
tCase.expectedUpdatedAt, tCase.expectedGroups)
events, err := manager.GetEvents(account.Id, userID)
if err != nil {
return
}
var ev *activity.Event
for _, event := range events {
if event.Activity == activity.SetupKeyCreated {
ev = event
}
}
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, tCase.expectedKeyName, ev.Meta["name"])
assert.Equal(t, tCase.expectedType, fmt.Sprint(ev.Meta["type"]))
assert.NotEmpty(t, ev.Meta["key"])
})
}

View File

@ -2,11 +2,13 @@ package server
import (
"fmt"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/status"
"strings"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
log "github.com/sirupsen/logrus"
"strings"
"time"
)
const (
@ -117,7 +119,7 @@ func NewAdminUser(id string) *User {
}
// CreateUser creates a new user under the given account. Effectively this is a user invite.
func (am *DefaultAccountManager) CreateUser(accountID string, invite *UserInfo) (*UserInfo, error) {
func (am *DefaultAccountManager) CreateUser(accountID, userID string, invite *UserInfo) (*UserInfo, error) {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -176,13 +178,26 @@ func (am *DefaultAccountManager) CreateUser(accountID string, invite *UserInfo)
return nil, err
}
event := &activity.Event{
Timestamp: time.Now(),
Activity: activity.UserInvited,
AccountID: account.Id,
TargetID: newUser.Id,
InitiatorID: userID,
}
_, err = am.eventStore.Save(event)
if err != nil {
return nil, err
}
return newUser.toUserInfo(idpUser)
}
// SaveUser saves updates a given user. If the user doesn't exit it will throw status.NotFound error.
// Only User.AutoGroups field is allowed to be updated for now.
func (am *DefaultAccountManager) SaveUser(accountID string, update *User) (*UserInfo, error) {
func (am *DefaultAccountManager) SaveUser(accountID, userID string, update *User) (*UserInfo, error) {
unlock := am.Store.AcquireAccountLock(accountID)
defer unlock()
@ -218,6 +233,68 @@ func (am *DefaultAccountManager) SaveUser(accountID string, update *User) (*User
return nil, err
}
defer func() {
if oldUser.Role != newUser.Role {
_, err := am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.UserRoleUpdated,
InitiatorID: userID,
TargetID: oldUser.Id,
AccountID: accountID,
Meta: map[string]any{"role": newUser.Role},
})
if err != nil {
log.Errorf("failed saving user activity event %v", err)
return
}
}
removedGroups := difference(oldUser.AutoGroups, update.AutoGroups)
addedGroups := difference(newUser.AutoGroups, oldUser.AutoGroups)
for _, g := range removedGroups {
group := account.GetGroup(g)
if group != nil {
_, err := am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupRemovedFromUser,
InitiatorID: userID,
TargetID: oldUser.Id,
AccountID: accountID,
Meta: map[string]any{"group": group.Name, "group_id": group.ID},
})
if err != nil {
log.Errorf("failed saving user activity event %s %v",
activity.GroupRemovedFromUser.StringCode(), err)
continue
}
} else {
log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)
}
}
for _, g := range addedGroups {
group := account.GetGroup(g)
if group != nil {
_, err := am.eventStore.Save(&activity.Event{
Timestamp: time.Now(),
Activity: activity.GroupAddedToUser,
InitiatorID: userID,
TargetID: oldUser.Id,
AccountID: accountID,
Meta: map[string]any{"group": group.Name, "group_id": group.ID},
})
if err != nil {
log.Errorf("failed saving user activity event %s: %v",
activity.GroupAddedToUser.StringCode(), err)
continue
}
} else {
log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)
}
}
}()
if !isNil(am.idpManager) {
userData, err := am.lookupUserInCache(newUser.Id, account)
if err != nil {