mirror of
https://github.com/netbirdio/netbird.git
synced 2025-01-23 14:28:51 +01:00
5c0b8a46f0
This PR adds system activity tracking. The management service records events like add/remove peer, group, rule, route, etc. The activity events are stored in the SQLite event store and can be queried by the HTTP API.
423 lines
11 KiB
Go
423 lines
11 KiB
Go
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"
|
|
log "github.com/sirupsen/logrus"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
UserRoleAdmin UserRole = "admin"
|
|
UserRoleUser UserRole = "user"
|
|
UserRoleUnknown UserRole = "unknown"
|
|
|
|
UserStatusActive UserStatus = "active"
|
|
UserStatusDisabled UserStatus = "disabled"
|
|
UserStatusInvited UserStatus = "invited"
|
|
)
|
|
|
|
// StrRoleToUserRole returns UserRole for a given strRole or UserRoleUnknown if the specified role is unknown
|
|
func StrRoleToUserRole(strRole string) UserRole {
|
|
switch strings.ToLower(strRole) {
|
|
case "admin":
|
|
return UserRoleAdmin
|
|
case "user":
|
|
return UserRoleUser
|
|
default:
|
|
return UserRoleUnknown
|
|
}
|
|
}
|
|
|
|
// UserStatus is the status of a User
|
|
type UserStatus string
|
|
|
|
// UserRole is the role of a User
|
|
type UserRole string
|
|
|
|
// User represents a user of the system
|
|
type User struct {
|
|
Id string
|
|
Role UserRole
|
|
// AutoGroups is a list of Group IDs to auto-assign to peers registered by this user
|
|
AutoGroups []string
|
|
}
|
|
|
|
// IsAdmin returns true if user is an admin, false otherwise
|
|
func (u *User) IsAdmin() bool {
|
|
return u.Role == UserRoleAdmin
|
|
}
|
|
|
|
// toUserInfo converts a User object to a UserInfo object.
|
|
func (u *User) toUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
|
autoGroups := u.AutoGroups
|
|
if autoGroups == nil {
|
|
autoGroups = []string{}
|
|
}
|
|
|
|
if userData == nil {
|
|
return &UserInfo{
|
|
ID: u.Id,
|
|
Email: "",
|
|
Name: "",
|
|
Role: string(u.Role),
|
|
AutoGroups: u.AutoGroups,
|
|
Status: string(UserStatusActive),
|
|
}, nil
|
|
}
|
|
if userData.ID != u.Id {
|
|
return nil, fmt.Errorf("wrong UserData provided for user %s", u.Id)
|
|
}
|
|
|
|
userStatus := UserStatusActive
|
|
if userData.AppMetadata.WTPendingInvite != nil && *userData.AppMetadata.WTPendingInvite {
|
|
userStatus = UserStatusInvited
|
|
}
|
|
|
|
return &UserInfo{
|
|
ID: u.Id,
|
|
Email: userData.Email,
|
|
Name: userData.Name,
|
|
Role: string(u.Role),
|
|
AutoGroups: autoGroups,
|
|
Status: string(userStatus),
|
|
}, nil
|
|
}
|
|
|
|
// Copy the user
|
|
func (u *User) Copy() *User {
|
|
autoGroups := make([]string, 0)
|
|
autoGroups = append(autoGroups, u.AutoGroups...)
|
|
return &User{
|
|
Id: u.Id,
|
|
Role: u.Role,
|
|
AutoGroups: autoGroups,
|
|
}
|
|
}
|
|
|
|
// NewUser creates a new user
|
|
func NewUser(id string, role UserRole) *User {
|
|
return &User{
|
|
Id: id,
|
|
Role: role,
|
|
AutoGroups: []string{},
|
|
}
|
|
}
|
|
|
|
// NewRegularUser creates a new user with role UserRoleAdmin
|
|
func NewRegularUser(id string) *User {
|
|
return NewUser(id, UserRoleUser)
|
|
}
|
|
|
|
// NewAdminUser creates a new user with role UserRoleAdmin
|
|
func NewAdminUser(id string) *User {
|
|
return NewUser(id, UserRoleAdmin)
|
|
}
|
|
|
|
// CreateUser creates a new user under the given account. Effectively this is a user invite.
|
|
func (am *DefaultAccountManager) CreateUser(accountID, userID string, invite *UserInfo) (*UserInfo, error) {
|
|
unlock := am.Store.AcquireAccountLock(accountID)
|
|
defer unlock()
|
|
|
|
if am.idpManager == nil {
|
|
return nil, status.Errorf(status.PreconditionFailed, "IdP manager must be enabled to send user invites")
|
|
}
|
|
|
|
if invite == nil {
|
|
return nil, fmt.Errorf("provided user update is nil")
|
|
}
|
|
|
|
account, err := am.Store.GetAccount(accountID)
|
|
if err != nil {
|
|
return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID)
|
|
}
|
|
|
|
// check if the user is already registered with this email => reject
|
|
user, err := am.lookupUserInCacheByEmail(invite.Email, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user != nil {
|
|
return nil, status.Errorf(status.UserAlreadyExists, "user has an existing account")
|
|
}
|
|
|
|
users, err := am.idpManager.GetUserByEmail(invite.Email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(users) > 0 {
|
|
return nil, status.Errorf(status.UserAlreadyExists, "user has an existing account")
|
|
}
|
|
|
|
idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
role := StrRoleToUserRole(invite.Role)
|
|
newUser := &User{
|
|
Id: idpUser.ID,
|
|
Role: role,
|
|
AutoGroups: invite.AutoGroups,
|
|
}
|
|
account.Users[idpUser.ID] = newUser
|
|
|
|
err = am.Store.SaveAccount(account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = am.refreshCache(account.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
event := &activity.Event{
|
|
Timestamp: time.Now(),
|
|
Activity: activity.UserInvited,
|
|
AccountID: account.Id,
|
|
TargetID: newUser.Id,
|
|
InitiatorID: userID,
|
|
}
|
|
|
|
_, err = am.eventStore.Save(event)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newUser.toUserInfo(idpUser)
|
|
|
|
}
|
|
|
|
// SaveUser saves updates a given user. If the user doesn't exit it will throw status.NotFound error.
|
|
// Only User.AutoGroups field is allowed to be updated for now.
|
|
func (am *DefaultAccountManager) SaveUser(accountID, userID string, update *User) (*UserInfo, error) {
|
|
unlock := am.Store.AcquireAccountLock(accountID)
|
|
defer unlock()
|
|
|
|
if update == nil {
|
|
return nil, status.Errorf(status.InvalidArgument, "provided user update is nil")
|
|
}
|
|
|
|
account, err := am.Store.GetAccount(accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, newGroupID := range update.AutoGroups {
|
|
if _, ok := account.Groups[newGroupID]; !ok {
|
|
return nil, status.Errorf(status.InvalidArgument, "provided group ID %s in the user %s update doesn't exist",
|
|
newGroupID, update.Id)
|
|
}
|
|
}
|
|
|
|
oldUser := account.Users[update.Id]
|
|
if oldUser == nil {
|
|
return nil, status.Errorf(status.NotFound, "update not found")
|
|
}
|
|
|
|
// only auto groups, revoked status, and name can be updated for now
|
|
newUser := oldUser.Copy()
|
|
newUser.AutoGroups = update.AutoGroups
|
|
newUser.Role = update.Role
|
|
|
|
account.Users[newUser.Id] = newUser
|
|
|
|
if err = am.Store.SaveAccount(account); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if oldUser.Role != newUser.Role {
|
|
_, err := am.eventStore.Save(&activity.Event{
|
|
Timestamp: time.Now(),
|
|
Activity: activity.UserRoleUpdated,
|
|
InitiatorID: userID,
|
|
TargetID: oldUser.Id,
|
|
AccountID: accountID,
|
|
Meta: map[string]any{"role": newUser.Role},
|
|
})
|
|
if err != nil {
|
|
log.Errorf("failed saving user activity event %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
removedGroups := difference(oldUser.AutoGroups, update.AutoGroups)
|
|
addedGroups := difference(newUser.AutoGroups, oldUser.AutoGroups)
|
|
for _, g := range removedGroups {
|
|
group := account.GetGroup(g)
|
|
if group != nil {
|
|
_, err := am.eventStore.Save(&activity.Event{
|
|
Timestamp: time.Now(),
|
|
Activity: activity.GroupRemovedFromUser,
|
|
InitiatorID: userID,
|
|
TargetID: oldUser.Id,
|
|
AccountID: accountID,
|
|
Meta: map[string]any{"group": group.Name, "group_id": group.ID},
|
|
})
|
|
if err != nil {
|
|
log.Errorf("failed saving user activity event %s %v",
|
|
activity.GroupRemovedFromUser.StringCode(), err)
|
|
continue
|
|
}
|
|
} else {
|
|
log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)
|
|
}
|
|
|
|
}
|
|
|
|
for _, g := range addedGroups {
|
|
group := account.GetGroup(g)
|
|
if group != nil {
|
|
_, err := am.eventStore.Save(&activity.Event{
|
|
Timestamp: time.Now(),
|
|
Activity: activity.GroupAddedToUser,
|
|
InitiatorID: userID,
|
|
TargetID: oldUser.Id,
|
|
AccountID: accountID,
|
|
Meta: map[string]any{"group": group.Name, "group_id": group.ID},
|
|
})
|
|
if err != nil {
|
|
log.Errorf("failed saving user activity event %s: %v",
|
|
activity.GroupAddedToUser.StringCode(), err)
|
|
continue
|
|
}
|
|
} else {
|
|
log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)
|
|
}
|
|
}
|
|
}()
|
|
|
|
if !isNil(am.idpManager) {
|
|
userData, err := am.lookupUserInCache(newUser.Id, account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if userData == nil {
|
|
return nil, status.Errorf(status.NotFound, "user %s not found in the IdP", newUser.Id)
|
|
}
|
|
return newUser.toUserInfo(userData)
|
|
}
|
|
return newUser.toUserInfo(nil)
|
|
}
|
|
|
|
// GetOrCreateAccountByUser returns an existing account for a given user id or creates a new one if doesn't exist
|
|
func (am *DefaultAccountManager) GetOrCreateAccountByUser(userID, domain string) (*Account, error) {
|
|
unlock := am.Store.AcquireGlobalLock()
|
|
defer unlock()
|
|
|
|
lowerDomain := strings.ToLower(domain)
|
|
|
|
account, err := am.Store.GetAccountByUser(userID)
|
|
if err != nil {
|
|
if s, ok := status.FromError(err); ok && s.Type() == status.NotFound {
|
|
account, err = am.newAccount(userID, lowerDomain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = am.Store.SaveAccount(account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// other error
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
userObj := account.Users[userID]
|
|
|
|
if account.Domain != lowerDomain && userObj.Role == UserRoleAdmin {
|
|
account.Domain = lowerDomain
|
|
err = am.Store.SaveAccount(account)
|
|
if err != nil {
|
|
return nil, status.Errorf(status.Internal, "failed updating account with domain")
|
|
}
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
// IsUserAdmin flag for current user authenticated by JWT token
|
|
func (am *DefaultAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) {
|
|
account, _, err := am.GetAccountFromToken(claims)
|
|
if err != nil {
|
|
return false, fmt.Errorf("get account: %v", err)
|
|
}
|
|
|
|
user, ok := account.Users[claims.UserId]
|
|
if !ok {
|
|
return false, status.Errorf(status.NotFound, "user not found")
|
|
}
|
|
|
|
return user.Role == UserRoleAdmin, nil
|
|
}
|
|
|
|
// GetUsersFromAccount performs a batched request for users from IDP by account ID apply filter on what data to return
|
|
// based on provided user role.
|
|
func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error) {
|
|
account, err := am.Store.GetAccount(accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := account.FindUser(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
queriedUsers := make([]*idp.UserData, 0)
|
|
if !isNil(am.idpManager) {
|
|
users := make(map[string]struct{}, len(account.Users))
|
|
for _, user := range account.Users {
|
|
users[user.Id] = struct{}{}
|
|
}
|
|
queriedUsers, err = am.lookupCache(users, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
userInfos := make([]*UserInfo, 0)
|
|
|
|
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
|
|
if len(queriedUsers) == 0 {
|
|
for _, accountUser := range account.Users {
|
|
if !user.IsAdmin() && user.Id != accountUser.Id {
|
|
// if user is not an admin then show only current user and do not show other users
|
|
continue
|
|
}
|
|
info, err := accountUser.toUserInfo(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userInfos = append(userInfos, info)
|
|
}
|
|
return userInfos, nil
|
|
}
|
|
|
|
for _, queriedUser := range queriedUsers {
|
|
if !user.IsAdmin() && user.Id != queriedUser.ID {
|
|
// if user is not an admin then show only current user and do not show other users
|
|
continue
|
|
}
|
|
if localUser, contains := account.Users[queriedUser.ID]; contains {
|
|
|
|
info, err := localUser.toUserInfo(queriedUser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userInfos = append(userInfos, info)
|
|
}
|
|
}
|
|
|
|
return userInfos, nil
|
|
}
|