diff --git a/management/server/event.go b/management/server/event.go index 788d1b51c..7bcf8ae25 100644 --- a/management/server/event.go +++ b/management/server/event.go @@ -10,6 +10,8 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" ) func isEnabled() bool { @@ -19,16 +21,12 @@ func isEnabled() bool { // GetEvents returns a list of activity events of an account func (am *DefaultAccountManager) GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error) { - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) - defer unlock() - - account, err := am.Store.GetAccount(ctx, accountID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { return nil, err } - user, err := account.FindUser(userID) - if err != nil { + if err := am.permissionsManager.ValidateAccountAccess(ctx, accountID, user); err != nil { return nil, err } @@ -58,6 +56,11 @@ func (am *DefaultAccountManager) GetEvents(ctx context.Context, accountID, userI filtered = append(filtered, event) } + err = am.fillEventsWithUserInfo(ctx, events, accountID, user) + if err != nil { + return nil, err + } + return filtered, nil } @@ -79,3 +82,145 @@ func (am *DefaultAccountManager) StoreEvent(ctx context.Context, initiatorID, ta }() } } + +type eventUserInfo struct { + email string + name string + accountId string +} + +func (am *DefaultAccountManager) fillEventsWithUserInfo(ctx context.Context, events []*activity.Event, accountId string, user *types.User) error { + eventUserInfo, err := am.getEventsUserInfo(ctx, events, accountId, user) + if err != nil { + return err + } + + for _, event := range events { + initiatorUserInfo, ok := eventUserInfo[event.InitiatorID] + if !ok { + log.WithContext(ctx).Warnf("failed to resolve user info for initiator: %s", event.InitiatorID) + } + + if event.InitiatorEmail == "" && ok { + event.InitiatorEmail = initiatorUserInfo.email + } + + if event.InitiatorName == "" && ok { + // here to allowed to be empty because in the first release we did not store the name + event.InitiatorName = initiatorUserInfo.name + } + + if ok && event.AccountID != initiatorUserInfo.accountId { + if event.Meta == nil { + event.Meta = make(map[string]any) + } + + event.Meta["external"] = true + } + + targetUserInfo, ok := eventUserInfo[event.TargetID] + if !ok { + continue + } + + if event.Meta == nil { + event.Meta = make(map[string]any) + } + + event.Meta["email"] = targetUserInfo.email + event.Meta["username"] = targetUserInfo.name + } + return nil +} + +func (am *DefaultAccountManager) getEventsUserInfo(ctx context.Context, events []*activity.Event, accountId string, user *types.User) (map[string]eventUserInfo, error) { + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountId) + if err != nil { + return nil, err + } + + // @note check whether using a external initiator user here is an issue + userInfos, err := am.BuildUserInfosForAccount(ctx, accountId, user.Id, accountUsers) + if err != nil { + return nil, err + } + + eventUserInfos := make(map[string]eventUserInfo) + for i, k := range userInfos { + eventUserInfos[i] = eventUserInfo{ + email: k.Email, + name: k.Name, + accountId: accountId, + } + } + + externalUserIds := []string{} + for _, event := range events { + if _, ok := eventUserInfos[event.InitiatorID]; ok { + continue + } + + if event.InitiatorID == activity.SystemInitiator || + event.InitiatorID == accountId || + event.Activity == activity.PeerAddedWithSetupKey { + // @todo other events to be excluded if never initiated by a user + continue + } + + externalUserIds = append(externalUserIds, event.InitiatorID) + } + + if len(externalUserIds) == 0 { + return eventUserInfos, nil + } + + return am.getEventsExternalUserInfo(ctx, externalUserIds, eventUserInfos, user) +} + +func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context, externalUserIds []string, eventUserInfos map[string]eventUserInfo, user *types.User) (map[string]eventUserInfo, error) { + externalAccountId := "" + fetched := make(map[string]struct{}) + externalUsers := []*types.User{} + for _, id := range externalUserIds { + if _, ok := fetched[id]; ok { + continue + } + + externalUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, id) + if err != nil { + continue + } + + if externalAccountId != "" && externalAccountId != externalUser.AccountID { + return nil, fmt.Errorf("multiple external user accounts in events") + } + + if externalAccountId == "" { + externalAccountId = externalUser.AccountID + } + + fetched[id] = struct{}{} + externalUsers = append(externalUsers, externalUser) + } + + // if we couldn't determine an account, return what we have + if externalAccountId == "" { + log.WithContext(ctx).Warnf("failed to determine external user account from users: %v", externalUserIds) + return eventUserInfos, nil + } + + externalUserInfos, err := am.BuildUserInfosForAccount(ctx, externalAccountId, user.Id, externalUsers) + if err != nil { + return nil, err + } + + for i, k := range externalUserInfos { + eventUserInfos[i] = eventUserInfo{ + email: k.Email, + name: k.Name, + accountId: externalAccountId, + } + } + + return eventUserInfos, nil +} diff --git a/management/server/http/handlers/events/events_handler.go b/management/server/http/handlers/events/events_handler.go index 7ebdef78f..eee5d8aa7 100644 --- a/management/server/http/handlers/events/events_handler.go +++ b/management/server/http/handlers/events/events_handler.go @@ -1,7 +1,6 @@ package events import ( - "context" "fmt" "net/http" @@ -47,66 +46,15 @@ func (h *handler) getAllEvents(w http.ResponseWriter, r *http.Request) { util.WriteError(r.Context(), err, w) return } + events := make([]*api.Event, len(accountEvents)) for i, e := range accountEvents { events[i] = toEventResponse(e) } - err = h.fillEventsWithUserInfo(r.Context(), events, accountID, userID) - if err != nil { - util.WriteError(r.Context(), err, w) - return - } - util.WriteJSONObject(r.Context(), w, events) } -func (h *handler) fillEventsWithUserInfo(ctx context.Context, events []*api.Event, accountId, userId string) error { - // build email, name maps based on users - userInfos, err := h.accountManager.GetUsersFromAccount(ctx, accountId, userId) - if err != nil { - log.WithContext(ctx).Errorf("failed to get users from account: %s", err) - return err - } - - emails := make(map[string]string) - names := make(map[string]string) - for _, ui := range userInfos { - emails[ui.ID] = ui.Email - names[ui.ID] = ui.Name - } - - var ok bool - for _, event := range events { - // fill initiator - if event.InitiatorEmail == "" { - event.InitiatorEmail, ok = emails[event.InitiatorId] - if !ok { - log.WithContext(ctx).Warnf("failed to resolve email for initiator: %s", event.InitiatorId) - } - } - - if event.InitiatorName == "" { - // here to allowed to be empty because in the first release we did not store the name - event.InitiatorName = names[event.InitiatorId] - } - - // fill target meta - email, ok := emails[event.TargetId] - if !ok { - continue - } - event.Meta["email"] = email - - username, ok := names[event.TargetId] - if !ok { - continue - } - event.Meta["username"] = username - } - return nil -} - func toEventResponse(event *activity.Event) *api.Event { meta := make(map[string]string) if event.Meta != nil {