From 1cfb7c42c600818b44448a6c33d9c7b3e4920e36 Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 27 Dec 2022 18:22:55 +0100 Subject: [PATCH 01/18] Add system activity tracking and event store --- .github/workflows/golang-test-linux.yml | 4 +- client/cmd/testutil.go | 8 +- client/internal/engine_test.go | 8 +- go.mod | 1 + go.sum | 2 + management/client/client_test.go | 8 +- management/cmd/management.go | 8 +- management/server/account.go | 57 ++++- management/server/account_test.go | 25 +- management/server/activity/event.go | 223 ++++++++++++++++++ management/server/activity/mock.go | 1 + management/server/activity/sqlite/sqlite.go | 145 ++++++++++++ .../server/activity/sqlite/sqlite_test.go | 52 ++++ management/server/event.go | 33 +++ management/server/group.go | 29 ++- management/server/grpcserver.go | 10 +- management/server/http/api/openapi.yml | 221 +++++++++++------ management/server/http/api/types.gen.go | 46 ++++ management/server/http/events.go | 69 ++++++ management/server/http/groups.go | 8 +- management/server/http/groups_test.go | 2 +- management/server/http/handler.go | 3 + management/server/http/peers.go | 15 +- management/server/http/rules.go | 12 +- management/server/http/rules_test.go | 2 +- management/server/http/setupkeys.go | 8 +- management/server/http/setupkeys_test.go | 4 +- management/server/http/users.go | 4 +- management/server/management_proto_test.go | 8 +- management/server/management_test.go | 8 +- management/server/mock_server/account_mock.go | 58 +++-- management/server/nameserver_test.go | 7 +- management/server/peer.go | 46 +++- management/server/peer_test.go | 18 +- management/server/route_test.go | 20 +- management/server/rule.go | 48 +++- management/server/setupkey.go | 48 +++- management/server/setupkey_test.go | 12 +- management/server/user.go | 20 +- 39 files changed, 1099 insertions(+), 202 deletions(-) create mode 100644 management/server/activity/event.go create mode 100644 management/server/activity/mock.go create mode 100644 management/server/activity/sqlite/sqlite.go create mode 100644 management/server/activity/sqlite/sqlite_test.go create mode 100644 management/server/event.go create mode 100644 management/server/http/events.go diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index dbb31ccdbf8..4186baf38c5 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -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 diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index 7bce7f5116f..e52e84f6318 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -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.NoopEventStore{} + if err != nil { + return nil, nil + } + accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", + eventStore) if err != nil { t.Fatal(err) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 895bfb3ac8e..64af22ee6bb 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -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.NoopEventStore{} + if err != nil { + return nil, nil + } + accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", + eventStore) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 9e673f9d274..e7d79eee9b3 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 498b585ee13..a8372e11d60 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/management/client/client_test.go b/management/client/client_test.go index 129dbf1ca03..c7932da28f7 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -2,6 +2,7 @@ package client import ( "context" + activity "github.com/netbirdio/netbird/management/server/activity/sqlite" "net" "path/filepath" "sync" @@ -55,7 +56,12 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { } peersUpdateManager := mgmt.NewPeersUpdateManager() - accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "") + eventStore, err := activity.NewSQLiteStore(t.TempDir()) + if err != nil { + return nil, nil + } + accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", + eventStore) if err != nil { t.Fatal(err) } diff --git a/management/cmd/management.go b/management/cmd/management.go index 82168ec72bb..b703c278b19 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -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) } diff --git a/management/server/account.go b/management/server/account.go index c4a7cd9569e..ad08f665a45 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -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,9 +40,9 @@ 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) GetSetupKey(accountID, userID, keyID string) (*SetupKey, 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. @@ -435,7 +438,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 +447,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 +514,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 +812,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 +856,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 + } }() } diff --git a/management/server/account_test.go b/management/server/account_test.go index 31480eb5d19..8150d5bc7b2 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3,6 +3,7 @@ package server import ( "fmt" nbdns "github.com/netbirdio/netbird/dns" + activity "github.com/netbirdio/netbird/management/server/activity/sqlite" "github.com/netbirdio/netbird/route" "net" "reflect" @@ -632,7 +633,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 +717,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 +742,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 +762,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 +782,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 +817,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, "") if err != nil { t.Fatal(err) } @@ -843,7 +846,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 } @@ -1228,7 +1231,11 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - return BuildManager(store, NewPeersUpdateManager(), nil, "", "") + eventStore, err := activity.NewSQLiteStore(t.TempDir()) + if err != nil { + return nil, err + } + return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } func createStore(t *testing.T) (Store, error) { diff --git a/management/server/activity/event.go b/management/server/activity/event.go new file mode 100644 index 00000000000..b80be9d0567 --- /dev/null +++ b/management/server/activity/event.go @@ -0,0 +1,223 @@ +package activity + +import "time" + +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 + // PeerGroupsUpdated indicates that a user updated groups of a peer + PeerGroupsUpdated +) + +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" + // PeerGroupsUpdatedMessage is a human-readable text message of the PeerGroupsUpdated activity + PeerGroupsUpdatedMessage string = "Peer groups updated" +) + +// 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 PeerGroupsUpdated: + return PeerGroupsUpdatedMessage + 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 PeerGroupsUpdated: + return "peer.groups.update" + default: + return "UNKNOWN_ACTIVITY" + } +} + +// 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 +} + +// NoopEventStore implements the Store interface without doing any operations +type NoopEventStore struct { +} + +// Save sets the Event.ID to 1 +func (store *NoopEventStore) Save(event *Event) (*Event, error) { + event.ID = 1 + return event, nil +} + +// Get returns an empty list of events +func (store *NoopEventStore) Get(accountID string, offset, limit int, descending bool) ([]*Event, error) { + return []*Event{}, nil +} + +// Close doesn't close anything +func (store *NoopEventStore) Close() error { + return nil +} + +// 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, + } +} diff --git a/management/server/activity/mock.go b/management/server/activity/mock.go new file mode 100644 index 00000000000..660931070b1 --- /dev/null +++ b/management/server/activity/mock.go @@ -0,0 +1 @@ +package activity diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go new file mode 100644 index 00000000000..2d76cd57012 --- /dev/null +++ b/management/server/activity/sqlite/sqlite.go @@ -0,0 +1,145 @@ +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);" +) + +// 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, ×tamp, &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("SELECT id, activity, timestamp, initiator_id, target_id, account_id, meta"+ + " FROM events WHERE account_id = ? ORDER BY timestamp %s LIMIT ? OFFSET ?;", 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("INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) VALUES(?, ?, ?, ?, ?, ?)") + 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 +} diff --git a/management/server/activity/sqlite/sqlite_test.go b/management/server/activity/sqlite/sqlite_test.go new file mode 100644 index 00000000000..305d50fe1ca --- /dev/null +++ b/management/server/activity/sqlite/sqlite_test.go @@ -0,0 +1,52 @@ +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 + } + + 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)) +} diff --git a/management/server/event.go b/management/server/event.go new file mode 100644 index 00000000000..3c52d94b33c --- /dev/null +++ b/management/server/event.go @@ -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 +} diff --git a/management/server/group.go b/management/server/group.go index cfc7844878e..8455ab4ea6e 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -1,6 +1,10 @@ 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" + "time" +) // Group of the peers for ACL type Group struct { @@ -34,6 +38,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 +71,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, group *Group) error { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -71,7 +80,7 @@ func (am *DefaultAccountManager) SaveGroup(accountID string, group *Group) error if err != nil { return err } - + _, exists := account.Groups[group.ID] account.Groups[group.ID] = group account.Network.IncSerial() @@ -79,6 +88,20 @@ func (am *DefaultAccountManager) SaveGroup(accountID string, group *Group) error return err } + if !exists { + _, err = am.eventStore.Save(&activity.Event{ + Timestamp: time.Now(), + Activity: activity.GroupCreated, + InitiatorID: userID, + TargetID: group.ID, + AccountID: accountID, + Meta: group.EventMeta(), + }) + if err != nil { + return err + } + } + return am.updateAccountPeers(account) } diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index f85a3c236a9..f0d66f641ae 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -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)}, diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index c25e14fa054..f452cfb68e4 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -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,72 @@ components: path: description: Nameserver group field to update in form / 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", + "setupkey.peer.add", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse", + "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 +577,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 +600,7 @@ paths: /api/users/: post: summary: Create a User (invite) - tags: [ Users] + tags: [ Users ] security: - BearerAuth: [ ] requestBody: @@ -585,7 +627,7 @@ paths: /api/users/{id}: put: summary: Update information about a User - tags: [ Users] + tags: [ Users ] security: - BearerAuth: [ ] parameters: @@ -619,9 +661,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 +684,7 @@ paths: /api/peers/{id}: get: summary: Get information about a peer - tags: [Peers] + tags: [ Peers ] security: - BearerAuth: [ ] parameters: @@ -669,7 +711,7 @@ paths: "$ref": "#/components/responses/internal_error" put: summary: Update information about a peer - tags: [Peers] + tags: [ Peers ] security: - BearerAuth: [ ] parameters: @@ -710,7 +752,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a peer - tags: [Peers] + tags: [ Peers ] security: - BearerAuth: [ ] parameters: @@ -723,7 +765,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -735,7 +777,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 +799,7 @@ paths: "$ref": "#/components/responses/internal_error" post: summary: Creates a Setup Key - tags: [Setup Keys] + tags: [ Setup Keys ] security: - BearerAuth: [ ] requestBody: @@ -784,7 +826,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 +853,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 +886,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Setup Key - tags: [Setup Keys] + tags: [ Setup Keys ] security: - BearerAuth: [ ] parameters: @@ -857,7 +899,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -869,7 +911,7 @@ paths: /api/groups: get: summary: Returns a list of all Groups - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] responses: @@ -891,7 +933,7 @@ paths: "$ref": "#/components/responses/internal_error" post: summary: Creates a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] requestBody: @@ -927,7 +969,7 @@ paths: /api/groups/{id}: get: summary: Get information about a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] parameters: @@ -954,7 +996,7 @@ paths: "$ref": "#/components/responses/internal_error" put: summary: Update/Replace a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] parameters: @@ -1029,7 +1071,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] parameters: @@ -1042,7 +1084,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -1054,7 +1096,7 @@ paths: /api/rules: get: summary: Returns a list of all Rules - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] responses: @@ -1076,7 +1118,7 @@ paths: "$ref": "#/components/responses/internal_error" post: summary: Creates a Rule - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] requestBody: @@ -1106,7 +1148,7 @@ paths: /api/rules/{id}: get: summary: Get information about a Rules - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] parameters: @@ -1133,7 +1175,7 @@ paths: "$ref": "#/components/responses/internal_error" put: summary: Update/Replace a Rule - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] parameters: @@ -1212,7 +1254,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Rule - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] parameters: @@ -1225,7 +1267,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -1573,5 +1615,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" \ No newline at end of file diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 3ba8e677587..e180265d07d 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -11,6 +11,25 @@ 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" + EventActivityCodeUserInvite EventActivityCode = "user.invite" + EventActivityCodeUserJoin EventActivityCode = "user.join" + EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add" + EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete" +) + // Defines values for GroupPatchOperationOp. const ( GroupPatchOperationOpAdd GroupPatchOperationOp = "add" @@ -97,6 +116,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 diff --git a/management/server/http/events.go b/management/server/http/events.go new file mode 100644 index 00000000000..a635f1c3db1 --- /dev/null +++ b/management/server/http/events.go @@ -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, + } +} diff --git a/management/server/http/groups.go b/management/server/http/groups.go index a636da375e9..8ee4f6bae57 100644 --- a/management/server/http/groups.go +++ b/management/server/http/groups.go @@ -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 diff --git a/management/server/http/groups_test.go b/management/server/http/groups_test.go index 4c6d5b0e035..25a66e47140 100644 --- a/management/server/http/groups_test.go +++ b/management/server/http/groups_test.go @@ -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" } diff --git a/management/server/http/handler.go b/management/server/http/handler.go index f0d0c5b4a03..d4beaec904e 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -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 { diff --git a/management/server/http/peers.go b/management/server/http/peers.go index 045efdb924d..0962b8b7de6 100644 --- a/management/server/http/peers.go +++ b/management/server/http/peers.go @@ -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(), diff --git a/management/server/http/rules.go b/management/server/http/rules.go index 9f12191853e..42f9a25b2c3 100644 --- a/management/server/http/rules.go +++ b/management/server/http/rules.go @@ -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 diff --git a/management/server/http/rules_test.go b/management/server/http/rules_test.go index f5b7a8b9830..9eade4c1fe4 100644 --- a/management/server/http/rules_test.go +++ b/management/server/http/rules_test.go @@ -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" } diff --git a/management/server/http/setupkeys.go b/management/server/http/setupkeys.go index a88cba29e84..cecc8603663 100644 --- a/management/server/http/setupkeys.go +++ b/management/server/http/setupkeys.go @@ -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 diff --git a/management/server/http/setupkeys_test.go b/management/server/http/setupkeys_test.go index 2b26ebb5a77..9e780b86fa7 100644 --- a/management/server/http/setupkeys_test.go +++ b/management/server/http/setupkeys_test.go @@ -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 } diff --git a/management/server/http/users.go b/management/server/http/users.go index 698bb94104e..7772e4f0fd3 100644 --- a/management/server/http/users.go +++ b/management/server/http/users.go @@ -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, diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 5dc20b0cb8a..426ce4b9192 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + activity "github.com/netbirdio/netbird/management/server/activity/sqlite" "net" "os" "path/filepath" @@ -403,7 +404,12 @@ func startManagement(t *testing.T, port int, config *Config) (*grpc.Server, erro return nil, err } peersUpdateManager := NewPeersUpdateManager() - accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "") + eventStore, err := activity.NewSQLiteStore(t.TempDir()) + if err != nil { + return nil, err + } + accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "", + eventStore) if err != nil { return nil, err } diff --git a/management/server/management_test.go b/management/server/management_test.go index f4c1b2ac002..25a48fad362 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -2,6 +2,7 @@ package server_test import ( "context" + activity "github.com/netbirdio/netbird/management/server/activity/sqlite" "math/rand" "net" "os" @@ -493,7 +494,12 @@ 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, err := activity.NewSQLiteStore(config.Datadir) + if err != nil { + log.Fatalf("failed creating a event store: %s: %v", config.Datadir, err) + } + accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", + eventStore) if err != nil { log.Fatalf("failed creating a manager: %v", err) } diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 8efeee33aa2..9e68774df60 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -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,7 +51,7 @@ 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) GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) @@ -58,9 +60,10 @@ type MockAccountManager struct { 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") @@ -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") +} diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 30cc8246f9d..5f3c91c8710 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -2,6 +2,7 @@ package server import ( nbdns "github.com/netbirdio/netbird/dns" + activity "github.com/netbirdio/netbird/management/server/activity/sqlite" "github.com/stretchr/testify/require" "net/netip" "testing" @@ -1056,7 +1057,11 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - return BuildManager(store, NewPeersUpdateManager(), nil, "", "") + eventStore, err := activity.NewSQLiteStore(t.TempDir()) + if err != nil { + return nil, err + } + return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } func createNSStore(t *testing.T) (Store, error) { diff --git a/management/server/peer.go b/management/server/peer.go index d4bc089dc75..1e4bbd4f2ee 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -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 } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 54f4629ba4f..28242cddcb7 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -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 diff --git a/management/server/route_test.go b/management/server/route_test.go index 1bd0598ccdf..6447cfb39e9 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1,6 +1,7 @@ package server import ( + activity "github.com/netbirdio/netbird/management/server/activity/sqlite" "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,11 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - return BuildManager(store, NewPeersUpdateManager(), nil, "", "") + eventStore, err := activity.NewSQLiteStore(t.TempDir()) + if err != nil { + return nil, err + } + return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } func createRouterStore(t *testing.T) (Store, error) { @@ -980,7 +985,6 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er } accountID := "testingAcc" - userID := "testingUser" domain := "example.com" account := newAccountWithId(accountID, userID, domain) @@ -1002,7 +1006,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 +1017,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 } diff --git a/management/server/rule.go b/management/server/rule.go index 98c74c02cf0..6111a05ce02 100644 --- a/management/server/rule.go +++ b/management/server/rule.go @@ -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) } diff --git a/management/server/setupkey.go b/management/server/setupkey.go index 824d381a2ec..5df434db8f9 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -2,6 +2,7 @@ package server import ( "github.com/google/uuid" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" "hash/fnv" "strconv" @@ -107,12 +108,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 +198,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 +220,23 @@ 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 + } + return setupKey, nil } @@ -224,7 +244,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 +281,20 @@ 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 + } + } + return newKey, am.updateAccountPeers(account) } @@ -282,7 +316,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 +358,7 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, userID, keyID string) (* } if !user.IsAdmin() { - foundKey = foundKey.HiddenCopy() + foundKey = foundKey.HiddenCopy(999) } return foundKey, nil diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index 290259116cd..41b3c1697bd 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -20,7 +20,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 +33,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,7 +46,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { Name: newKeyName, Revoked: revoked, AutoGroups: autoGroups, - }) + }, userID) if err != nil { t.Fatal(err) } @@ -67,7 +67,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 +76,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 +121,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 { diff --git a/management/server/user.go b/management/server/user.go index 82775c72657..9e8a3fa59d4 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -2,11 +2,12 @@ package server import ( "fmt" + "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" "strings" - - "github.com/netbirdio/netbird/management/server/jwtclaims" + "time" ) const ( @@ -117,7 +118,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,6 +177,19 @@ 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) } From ff65a51941fa2a9af90fc2aac87627d7fac2011f Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Tue, 27 Dec 2022 19:39:04 +0100 Subject: [PATCH 02/18] Cleanup sqlite event store in tests --- management/server/activity/sqlite/sqlite_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/management/server/activity/sqlite/sqlite_test.go b/management/server/activity/sqlite/sqlite_test.go index 305d50fe1ca..f42c94d36e5 100644 --- a/management/server/activity/sqlite/sqlite_test.go +++ b/management/server/activity/sqlite/sqlite_test.go @@ -15,6 +15,7 @@ func TestNewSQLiteStore(t *testing.T) { t.Fatal(err) return } + defer store.Close() //nolint accountID := "account_1" From da95c7b1eb387b0c64429317c6dc0e6d91ddd305 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 28 Dec 2022 09:40:54 +0100 Subject: [PATCH 03/18] Use NoopEventStore in tests --- management/client/client_test.go | 7 ++----- management/cmd/management.go | 1 + management/server/account_test.go | 7 ++----- management/server/management_proto_test.go | 7 ++----- management/server/management_test.go | 7 ++----- management/server/nameserver_test.go | 7 ++----- management/server/route_test.go | 7 ++----- 7 files changed, 13 insertions(+), 30 deletions(-) diff --git a/management/client/client_test.go b/management/client/client_test.go index c7932da28f7..fc15943c4b7 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -2,7 +2,7 @@ package client import ( "context" - activity "github.com/netbirdio/netbird/management/server/activity/sqlite" + "github.com/netbirdio/netbird/management/server/activity" "net" "path/filepath" "sync" @@ -56,10 +56,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { } peersUpdateManager := mgmt.NewPeersUpdateManager() - eventStore, err := activity.NewSQLiteStore(t.TempDir()) - if err != nil { - return nil, nil - } + eventStore := &activity.NoopEventStore{} accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", eventStore) if err != nil { diff --git a/management/cmd/management.go b/management/cmd/management.go index b703c278b19..da8bfe5f90d 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -257,6 +257,7 @@ var ( } gRPCAPIHandler.Stop() _ = store.Close() + _ = eventStore.Close() log.Infof("stopped Management Service") return nil diff --git a/management/server/account_test.go b/management/server/account_test.go index 8150d5bc7b2..0306912c4fb 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3,7 +3,7 @@ package server import ( "fmt" nbdns "github.com/netbirdio/netbird/dns" - activity "github.com/netbirdio/netbird/management/server/activity/sqlite" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/route" "net" "reflect" @@ -1231,10 +1231,7 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - eventStore, err := activity.NewSQLiteStore(t.TempDir()) - if err != nil { - return nil, err - } + eventStore := &activity.NoopEventStore{} return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 426ce4b9192..95fcaec98db 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -3,7 +3,7 @@ package server import ( "context" "fmt" - activity "github.com/netbirdio/netbird/management/server/activity/sqlite" + "github.com/netbirdio/netbird/management/server/activity" "net" "os" "path/filepath" @@ -404,10 +404,7 @@ func startManagement(t *testing.T, port int, config *Config) (*grpc.Server, erro return nil, err } peersUpdateManager := NewPeersUpdateManager() - eventStore, err := activity.NewSQLiteStore(t.TempDir()) - if err != nil { - return nil, err - } + eventStore := &activity.NoopEventStore{} accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "", eventStore) if err != nil { diff --git a/management/server/management_test.go b/management/server/management_test.go index 25a48fad362..91d184c9efb 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -2,7 +2,7 @@ package server_test import ( "context" - activity "github.com/netbirdio/netbird/management/server/activity/sqlite" + "github.com/netbirdio/netbird/management/server/activity" "math/rand" "net" "os" @@ -494,10 +494,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } peersUpdateManager := server.NewPeersUpdateManager() - eventStore, err := activity.NewSQLiteStore(config.Datadir) - if err != nil { - log.Fatalf("failed creating a event store: %s: %v", config.Datadir, err) - } + eventStore := &activity.NoopEventStore{} accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", eventStore) if err != nil { diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 5f3c91c8710..2fad5801875 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -2,7 +2,7 @@ package server import ( nbdns "github.com/netbirdio/netbird/dns" - activity "github.com/netbirdio/netbird/management/server/activity/sqlite" + "github.com/netbirdio/netbird/management/server/activity" "github.com/stretchr/testify/require" "net/netip" "testing" @@ -1057,10 +1057,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - eventStore, err := activity.NewSQLiteStore(t.TempDir()) - if err != nil { - return nil, err - } + eventStore := &activity.NoopEventStore{} return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } diff --git a/management/server/route_test.go b/management/server/route_test.go index 6447cfb39e9..c231a72fc45 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1,7 +1,7 @@ package server import ( - activity "github.com/netbirdio/netbird/management/server/activity/sqlite" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/route" "github.com/rs/xid" "github.com/stretchr/testify/require" @@ -937,10 +937,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - eventStore, err := activity.NewSQLiteStore(t.TempDir()) - if err != nil { - return nil, err - } + eventStore := &activity.NoopEventStore{} return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } From 6c0e16070ec9d151986d32a95fe83a60700da2a9 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 28 Dec 2022 11:10:05 +0100 Subject: [PATCH 04/18] Add GetEvents test to account manager --- management/server/activity/event.go | 36 ++++++++++++++--- management/server/event_test.go | 63 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 management/server/event_test.go diff --git a/management/server/activity/event.go b/management/server/activity/event.go index b80be9d0567..b082b20744f 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -1,6 +1,9 @@ package activity -import "time" +import ( + "sync" + "time" +) const ( // PeerAddedByUser indicates that a user added a new peer to the system @@ -165,23 +168,44 @@ type Store interface { Close() error } -// NoopEventStore implements the Store interface without doing any operations +// NoopEventStore implements the Store interface storing data in-memory type NoopEventStore struct { + mu sync.Mutex + nextID uint64 + events []*Event } // Save sets the Event.ID to 1 func (store *NoopEventStore) Save(event *Event) (*Event, error) { - event.ID = 1 + 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 an empty list of events +// Get returns a list of ALL events that belong to the given accountID without taking offset, limit and order into consideration func (store *NoopEventStore) Get(accountID string, offset, limit int, descending bool) ([]*Event, error) { - return []*Event{}, nil + 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 doesn't close anything +// Close cleans up the event list func (store *NoopEventStore) Close() error { + store.mu.Lock() + defer store.mu.Unlock() + store.events = make([]*Event, 0) return nil } diff --git a/management/server/event_test.go b/management/server/event_test.go new file mode 100644 index 00000000000..be6dd700529 --- /dev/null +++ b/management/server/event_test.go @@ -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 + }) +} From 97c24c813449ee1048eab1e547a9a335fb75a4f4 Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 11:41:41 +0100 Subject: [PATCH 05/18] Add GetEvents HTTP API test --- management/server/http/events_test.go | 250 ++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 management/server/http/events_test.go diff --git a/management/server/http/events_test.go b/management/server/http/events_test.go new file mode 100644 index 00000000000..4b9ac29eedb --- /dev/null +++ b/management/server/http/events_test.go @@ -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{}, + }) + 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"}, + }) + ID++ + 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.True(t, expected.Timestamp.Equal(event.Timestamp)) + } + + }) + } +} From a42c9303dbd082f204d1ba40f05c71fbce8ecf48 Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 11:43:04 +0100 Subject: [PATCH 06/18] Add GetEvents HTTP API test --- management/server/http/events_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/management/server/http/events_test.go b/management/server/http/events_test.go index 4b9ac29eedb..d2d67d57333 100644 --- a/management/server/http/events_test.go +++ b/management/server/http/events_test.go @@ -59,7 +59,7 @@ func generateEvents(accountID, userID string) []*activity.Event { InitiatorID: userID, TargetID: "100.64.0.2", AccountID: accountID, - Meta: map[string]any{}, + Meta: map[string]any{"some": "meta"}, }) ID++ events = append(events, &activity.Event{ @@ -242,6 +242,7 @@ func TestEvents_GetEvents(t *testing.T) { 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)) } From d423468110d7a7beb5b254848bb260406b64a83f Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 11:56:55 +0100 Subject: [PATCH 07/18] Add SetupKey event tests --- management/server/setupkey_test.go | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index 41b3c1697bd..1a2772da003 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -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" @@ -53,6 +55,24 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { 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"]) } func TestDefaultAccountManager_CreateSetupKey(t *testing.T) { @@ -137,6 +157,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"]) }) } From 0d04e80b6b792a08bc2a33ecf366625a94bd2086 Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 11:58:56 +0100 Subject: [PATCH 08/18] Fix golint --- management/server/http/events_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/management/server/http/events_test.go b/management/server/http/events_test.go index d2d67d57333..c73d54c2fff 100644 --- a/management/server/http/events_test.go +++ b/management/server/http/events_test.go @@ -171,7 +171,6 @@ func generateEvents(accountID, userID string) []*activity.Event { AccountID: accountID, Meta: map[string]any{"some": "meta"}, }) - ID++ return events } From 18548d910aa02a77a6160359502d6c65881fad42 Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 12:09:56 +0100 Subject: [PATCH 09/18] Add more check for setup key events --- management/server/setupkey_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index 1a2772da003..a2dc362ca10 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -73,6 +73,8 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) { 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) { From dad2bf2a0a3b3e73000c6fae5fee3a3597c1b2ac Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 12:16:58 +0100 Subject: [PATCH 10/18] Add peer and account events test --- management/server/account_test.go | 113 +++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 0306912c4fb..c0fdf14b78e 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -113,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) { @@ -501,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) } @@ -562,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) { @@ -571,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) } @@ -591,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, @@ -624,6 +663,27 @@ func TestAccountManager_AddPeerWithUserID(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.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) { @@ -818,7 +878,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { return } userID := "account_creator" - account, err := createAccount(manager, "test_account", userID, "") + account, err := createAccount(manager, "test_account", userID, "netbird.cloud") if err != nil { t.Fatal(err) } @@ -836,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, @@ -860,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) { @@ -1232,7 +1313,7 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) { return nil, err } eventStore := &activity.NoopEventStore{} - return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) + return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore) } func createStore(t *testing.T) (Store, error) { From 5d891199dc64b17a5dfd772ae7e28f35a8f2eb6b Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 13:19:47 +0100 Subject: [PATCH 11/18] Fix Codacy --- management/server/account_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index c0fdf14b78e..7f9e31c17fd 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -630,7 +630,7 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) { return } expectedPeerKey := key.PublicKey().String() - expectedUserId := userID + expectedUserID := userID peer, err := manager.AddPeer("", userID, &Peer{ Key: expectedPeerKey, @@ -656,8 +656,8 @@ 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 { From c4e31760f2cfc39f79d771402a939a0e16b703c5 Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 29 Dec 2022 15:09:08 +0100 Subject: [PATCH 12/18] Add peer group events --- management/server/activity/event.go | 24 ++++++--- management/server/group.go | 75 +++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/management/server/activity/event.go b/management/server/activity/event.go index b082b20744f..2ab1b19a5e8 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -36,8 +36,10 @@ const ( GroupCreated // GroupUpdated indicates that a user updated a group GroupUpdated - // PeerGroupsUpdated indicates that a user updated groups of a peer - PeerGroupsUpdated + // GroupAddedToPeer indicates that a user added group to a peer + GroupAddedToPeer + // GroupRemovedFromPeer indicates that a user removed peer group + GroupRemovedFromPeer ) const ( @@ -71,8 +73,10 @@ const ( GroupCreatedMessage string = "Group created" // GroupUpdatedMessage is a human-readable text message of the GroupUpdated activity GroupUpdatedMessage string = "Group updated" - // PeerGroupsUpdatedMessage is a human-readable text message of the PeerGroupsUpdated activity - PeerGroupsUpdatedMessage string = "Peer groups 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" ) // Activity that triggered an Event @@ -111,8 +115,10 @@ func (a Activity) Message() string { return GroupCreatedMessage case GroupUpdated: return GroupUpdatedMessage - case PeerGroupsUpdated: - return PeerGroupsUpdatedMessage + case GroupAddedToPeer: + return GroupAddedToPeerMessage + case GroupRemovedFromPeer: + return GroupRemovedFromPeerMessage default: return "UNKNOWN_ACTIVITY" } @@ -151,8 +157,10 @@ func (a Activity) StringCode() string { return "group.add" case GroupUpdated: return "group.update" - case PeerGroupsUpdated: - return "peer.groups.update" + case GroupRemovedFromPeer: + return "peer.group.delete" + case GroupAddedToPeer: + return "peer.group.add" default: return "UNKNOWN_ACTIVITY" } diff --git a/management/server/group.go b/management/server/group.go index 8455ab4ea6e..b2f60b4bfd7 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -3,6 +3,7 @@ package server import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" + log "github.com/sirupsen/logrus" "time" ) @@ -71,7 +72,7 @@ func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, er } // SaveGroup object of the peers -func (am *DefaultAccountManager) SaveGroup(accountID, userID string, group *Group) error { +func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *Group) error { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -80,8 +81,8 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, group *Grou if err != nil { return err } - _, exists := account.Groups[group.ID] - 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 { @@ -93,9 +94,58 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, group *Grou Timestamp: time.Now(), Activity: activity.GroupCreated, InitiatorID: userID, - TargetID: group.ID, + TargetID: newGroup.ID, AccountID: accountID, - Meta: group.EventMeta(), + 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 @@ -105,6 +155,21 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, group *Grou 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) { From ba9eea5ea14d046773b92728e35384e3ddb2f220 Mon Sep 17 00:00:00 2001 From: braginini Date: Fri, 30 Dec 2022 16:10:31 +0100 Subject: [PATCH 13/18] Refactor activity package --- client/cmd/testutil.go | 2 +- client/internal/engine_test.go | 2 +- management/client/client_test.go | 2 +- management/server/account_test.go | 2 +- management/server/activity/codes.go | 186 ++++++++++++++++++ management/server/activity/event.go | 213 --------------------- management/server/activity/mock.go | 1 - management/server/activity/store.go | 54 ++++++ management/server/http/api/openapi.yml | 3 +- management/server/http/api/types.gen.go | 2 + management/server/management_proto_test.go | 2 +- management/server/management_test.go | 2 +- management/server/nameserver_test.go | 2 +- management/server/route_test.go | 2 +- 14 files changed, 252 insertions(+), 223 deletions(-) create mode 100644 management/server/activity/codes.go delete mode 100644 management/server/activity/mock.go create mode 100644 management/server/activity/store.go diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index e52e84f6318..4c923c70452 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -69,7 +69,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste } peersUpdateManager := mgmt.NewPeersUpdateManager() - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} if err != nil { return nil, nil } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 64af22ee6bb..cc4ff6401f6 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -954,7 +954,7 @@ func startManagement(port int, dataDir string) (*grpc.Server, error) { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } peersUpdateManager := server.NewPeersUpdateManager() - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} if err != nil { return nil, nil } diff --git a/management/client/client_test.go b/management/client/client_test.go index fc15943c4b7..92edd75512a 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -56,7 +56,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { } peersUpdateManager := mgmt.NewPeersUpdateManager() - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", eventStore) if err != nil { diff --git a/management/server/account_test.go b/management/server/account_test.go index 7f9e31c17fd..353fe7528e3 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1312,7 +1312,7 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore) } diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go new file mode 100644 index 00000000000..6e7a363c368 --- /dev/null +++ b/management/server/activity/codes.go @@ -0,0 +1,186 @@ +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 +) + +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" +) + +// 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 + 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" + default: + return "UNKNOWN_ACTIVITY" + } +} diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 2ab1b19a5e8..66844917698 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -1,222 +1,9 @@ package activity import ( - "sync" "time" ) -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 -) - -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" -) - -// 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 - 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" - default: - return "UNKNOWN_ACTIVITY" - } -} - -// 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 -} - -// NoopEventStore implements the Store interface storing data in-memory -type NoopEventStore struct { - mu sync.Mutex - nextID uint64 - events []*Event -} - -// Save sets the Event.ID to 1 -func (store *NoopEventStore) 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 *NoopEventStore) 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 *NoopEventStore) Close() error { - store.mu.Lock() - defer store.mu.Unlock() - store.events = make([]*Event, 0) - return nil -} - // Event represents a network/system activity event. type Event struct { // Timestamp of the event diff --git a/management/server/activity/mock.go b/management/server/activity/mock.go deleted file mode 100644 index 660931070b1..00000000000 --- a/management/server/activity/mock.go +++ /dev/null @@ -1 +0,0 @@ -package activity diff --git a/management/server/activity/store.go b/management/server/activity/store.go new file mode 100644 index 00000000000..77439e2e16e --- /dev/null +++ b/management/server/activity/store.go @@ -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 +} diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index f452cfb68e4..e3c8cb6a690 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -519,7 +519,8 @@ components: 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", + 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", "rule.add", "rule.delete", "rule.update", "group.add", "group.update", diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index e180265d07d..2f85e754a46 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -24,6 +24,8 @@ const ( 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" diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 95fcaec98db..e732191e008 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -404,7 +404,7 @@ func startManagement(t *testing.T, port int, config *Config) (*grpc.Server, erro return nil, err } peersUpdateManager := NewPeersUpdateManager() - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "", eventStore) if err != nil { diff --git a/management/server/management_test.go b/management/server/management_test.go index 91d184c9efb..565451548e8 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -494,7 +494,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } peersUpdateManager := server.NewPeersUpdateManager() - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", eventStore) if err != nil { diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 2fad5801875..f0f8ea06c86 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -1057,7 +1057,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } diff --git a/management/server/route_test.go b/management/server/route_test.go index c231a72fc45..2d64c0cd7ba 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -937,7 +937,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { if err != nil { return nil, err } - eventStore := &activity.NoopEventStore{} + eventStore := &activity.InMemoryEventStore{} return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore) } From ef6385bf836c509523548db705e040e932be8bad Mon Sep 17 00:00:00 2001 From: braginini Date: Fri, 30 Dec 2022 16:27:07 +0100 Subject: [PATCH 14/18] Add User group add/remove activity --- management/server/account.go | 7 ++- management/server/http/api/types.gen.go | 1 + management/server/http/users.go | 4 +- management/server/user.go | 66 ++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index ad08f665a45..614292f4691 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -44,7 +44,7 @@ type AccountManager interface { 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) @@ -254,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 diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 2f85e754a46..6fe25d1df20 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -30,6 +30,7 @@ const ( EventActivityCodeUserJoin EventActivityCode = "user.join" EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add" EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete" + EventActivityCodeUserRoleUpdate EventActivityCode = "user.role.update" ) // Defines values for GroupPatchOperationOp. diff --git a/management/server/http/users.go b/management/server/http/users.go index 7772e4f0fd3..d89c7981f05 100644 --- a/management/server/http/users.go +++ b/management/server/http/users.go @@ -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, diff --git a/management/server/user.go b/management/server/user.go index 9e8a3fa59d4..b887d7ed94d 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -6,6 +6,7 @@ import ( "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" + log "github.com/sirupsen/logrus" "strings" "time" ) @@ -196,7 +197,7 @@ func (am *DefaultAccountManager) CreateUser(accountID, userID string, invite *Us // 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() @@ -232,6 +233,69 @@ 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) + return + } + } 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) + return + } + } 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 { From cd9ead025e6995faf22e44429387b209b83707b5 Mon Sep 17 00:00:00 2001 From: braginini Date: Fri, 30 Dec 2022 16:46:57 +0100 Subject: [PATCH 15/18] Fix tests --- management/server/mock_server/account_mock.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 9e68774df60..2e8249573dd 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -53,7 +53,7 @@ type MockAccountManager struct { ListRoutesFunc func(accountID, userID string) ([]*route.Route, 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 @@ -404,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") } From 324095aa8dda0fd4b3ec6a1cdb56b13bc4aa1194 Mon Sep 17 00:00:00 2001 From: braginini Date: Fri, 30 Dec 2022 16:52:54 +0100 Subject: [PATCH 16/18] Extract sqlite statements to const --- management/server/activity/sqlite/sqlite.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go index 2d76cd57012..02c2143e437 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/sqlite/sqlite.go @@ -23,6 +23,11 @@ const ( "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 @@ -89,8 +94,7 @@ func (store *Store) Get(accountID string, offset, limit int, descending bool) ([ if !descending { order = "ASC" } - stmt, err := store.db.Prepare(fmt.Sprintf("SELECT id, activity, timestamp, initiator_id, target_id, account_id, meta"+ - " FROM events WHERE account_id = ? ORDER BY timestamp %s LIMIT ? OFFSET ?;", order)) + stmt, err := store.db.Prepare(fmt.Sprintf(selectStatement, order)) if err != nil { return nil, err } @@ -107,7 +111,7 @@ func (store *Store) Get(accountID string, offset, limit int, descending bool) ([ // Save an event in the SQLite events table func (store *Store) Save(event *activity.Event) (*activity.Event, error) { - stmt, err := store.db.Prepare("INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) VALUES(?, ?, ?, ?, ?, ?)") + stmt, err := store.db.Prepare(insertStatement) if err != nil { return nil, err } From 0d7bdad10c2e90e0992190c0e4155363e154aec1 Mon Sep 17 00:00:00 2001 From: braginini Date: Fri, 30 Dec 2022 17:48:43 +0100 Subject: [PATCH 17/18] Add SetupKey add/remove group activity events --- management/server/activity/codes.go | 16 +++++++++ management/server/http/api/openapi.yml | 1 + management/server/setupkey.go | 48 ++++++++++++++++++++++++++ management/server/user.go | 1 - 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 6e7a363c368..6ebe0bc2ee2 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -41,6 +41,10 @@ const ( 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 ( @@ -84,6 +88,10 @@ const ( 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 @@ -132,6 +140,10 @@ func (a Activity) Message() string { return GroupAddedToUserMessage case UserRoleUpdated: return UserRoleUpdatedMessage + case GroupAddedToSetupKey: + return GroupAddedToSetupKeyMessage + case GroupRemovedFromSetupKey: + return GroupRemovedFromSetupKeyMessage default: return "UNKNOWN_ACTIVITY" } @@ -180,6 +192,10 @@ func (a Activity) StringCode() string { 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" } diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index e3c8cb6a690..6071d178839 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -522,6 +522,7 @@ components: 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", diff --git a/management/server/setupkey.go b/management/server/setupkey.go index 5df434db8f9..d722a095177 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -4,6 +4,7 @@ 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" @@ -295,6 +296,53 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup } } + 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) + return + } + } 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) + return + } + } else { + log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id) + } + } + }() + return newKey, am.updateAccountPeers(account) } diff --git a/management/server/user.go b/management/server/user.go index b887d7ed94d..e12d050e8c2 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -292,7 +292,6 @@ func (am *DefaultAccountManager) SaveUser(accountID, userID string, update *User } else { log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id) } - } }() From 2860fd6a06f87dcdd5d1b9dc7cebe1d54b25b2c2 Mon Sep 17 00:00:00 2001 From: braginini Date: Fri, 30 Dec 2022 17:52:24 +0100 Subject: [PATCH 18/18] Add SetupKey add/remove group activity events --- management/server/setupkey.go | 25 +++++++++++++++++++++++-- management/server/user.go | 4 ++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/management/server/setupkey.go b/management/server/setupkey.go index d722a095177..16489c9109c 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -238,6 +238,27 @@ func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string 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 } @@ -313,7 +334,7 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup if err != nil { log.Errorf("failed saving setup key activity event %s: %v", activity.GroupRemovedFromSetupKey.StringCode(), err) - return + continue } } else { log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id) @@ -335,7 +356,7 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup if err != nil { log.Errorf("failed saving setup key activity event %s: %v", activity.GroupAddedToSetupKey.StringCode(), err) - return + continue } } else { log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id) diff --git a/management/server/user.go b/management/server/user.go index e12d050e8c2..84b24a1bfdb 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -265,7 +265,7 @@ func (am *DefaultAccountManager) SaveUser(accountID, userID string, update *User if err != nil { log.Errorf("failed saving user activity event %s %v", activity.GroupRemovedFromUser.StringCode(), err) - return + continue } } else { log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id) @@ -287,7 +287,7 @@ func (am *DefaultAccountManager) SaveUser(accountID, userID string, update *User if err != nil { log.Errorf("failed saving user activity event %s: %v", activity.GroupAddedToUser.StringCode(), err) - return + continue } } else { log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)