diff --git a/management/server/account.go b/management/server/account.go index f9c0a6ee2..90ab2108f 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -53,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) @@ -71,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) diff --git a/management/server/activity/event.go b/management/server/activity/event.go index f20d4cfe8..1584abe11 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -13,19 +13,35 @@ const ( 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 ) const ( // PeerAddedByUserMessage is a human-readable text message of the PeerAddedByUser activity - PeerAddedByUserMessage string = "User added a new peer" + PeerAddedByUserMessage string = "Peer added" // PeerAddedWithSetupKeyMessage is a human-readable text message of the PeerAddedWithSetupKey activity - PeerAddedWithSetupKeyMessage = "New peer added with a setup key" + PeerAddedWithSetupKeyMessage = PeerAddedByUserMessage //UserJoinedMessage is a human-readable text message of the UserJoined activity - UserJoinedMessage string = "New user joined" + UserJoinedMessage string = "User joined" //UserInvitedMessage is a human-readable text message of the UserInvited activity - UserInvitedMessage string = "New user invited" + 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" ) // Activity that triggered an Event @@ -36,6 +52,8 @@ func (a Activity) Message() string { switch a { case PeerAddedByUser: return PeerAddedByUserMessage + case PeerRemovedByUser: + return PeerRemovedByUserMessage case PeerAddedWithSetupKey: return PeerAddedWithSetupKeyMessage case UserJoined: @@ -44,6 +62,12 @@ func (a Activity) Message() string { return UserInvitedMessage case AccountCreated: return AccountCreatedMessage + case RuleAdded: + return RuleAddedMessage + case RuleRemoved: + return RuleRemovedMessage + case RuleUpdated: + return RuleUpdatedMessage default: return "UNKNOWN_ACTIVITY" } @@ -54,6 +78,8 @@ 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: @@ -62,6 +88,12 @@ func (a Activity) StringCode() string { return "user.invite" case AccountCreated: return "account.create" + case RuleAdded: + return "rule.add" + case RuleRemoved: + return "rule.delete" + case RuleUpdated: + return "rule.update" default: return "UNKNOWN_ACTIVITY" } @@ -91,10 +123,18 @@ type Event struct { 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, @@ -102,5 +142,6 @@ func (e *Event) Copy() *Event { InitiatorID: e.InitiatorID, TargetID: e.TargetID, AccountID: e.AccountID, + Meta: meta, } } diff --git a/management/server/activity/sqlite.go b/management/server/activity/sqlite.go index c46a633bb..0eec94af0 100644 --- a/management/server/activity/sqlite.go +++ b/management/server/activity/sqlite.go @@ -2,6 +2,7 @@ package activity import ( "database/sql" + "encoding/json" "fmt" _ "github.com/mattn/go-sqlite3" "path/filepath" @@ -16,6 +17,7 @@ const ( "timestamp DATETIME, " + "initiator_id TEXT," + "account_id TEXT," + + "meta TEXT," + " target_id TEXT);" ) @@ -49,11 +51,20 @@ func processResult(result *sql.Rows) ([]*Event, error) { var initiator string var target string var account string - err := result.Scan(&id, &operation, ×tamp, &initiator, &target, &account) + 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, &Event{ Timestamp: timestamp, Activity: operation, @@ -61,6 +72,7 @@ func processResult(result *sql.Rows) ([]*Event, error) { InitiatorID: initiator, TargetID: target, AccountID: account, + Meta: meta, }) } @@ -73,7 +85,7 @@ func (store *SQLiteStore) Get(accountID string, offset, limit int, descending bo if !descending { order = "ASC" } - stmt, err := store.db.Prepare(fmt.Sprintf("SELECT id, activity, timestamp, initiator_id, target_id, account_id"+ + 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 @@ -91,12 +103,21 @@ func (store *SQLiteStore) Get(accountID string, offset, limit int, descending bo // Save an event in the SQLite events table func (store *SQLiteStore) Save(event *Event) (*Event, error) { - stmt, err := store.db.Prepare("INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id) VALUES(?, ?, ?, ?, ?)") + stmt, err := store.db.Prepare("INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) VALUES(?, ?, ?, ?, ?, ?)") if err != nil { return nil, err } - result, err := stmt.Exec(event.Activity, event.Timestamp, event.InitiatorID, event.TargetID, event.AccountID) + 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 } diff --git a/management/server/activity/sqlite_test.go b/management/server/activity/sqlite_test.go index 4fb7d76ef..5d49aa3e9 100644 --- a/management/server/activity/sqlite_test.go +++ b/management/server/activity/sqlite_test.go @@ -1,29 +1,27 @@ package activity import ( - "fmt" - "github.com/stretchr/testify/assert" "testing" "time" ) func TestNewSQLiteStore(t *testing.T) { - dataDir := t.TempDir() - store, err := NewSQLiteStore(dataDir) + //dataDir := t.TempDir() + store, err := NewSQLiteStore("/home/braginini/wiretrustee/test/") if err != nil { t.Fatal(err) return } - accountID := "account_1" + //accountID := "account_1" - for i := 0; i < 10; i++ { + for i := 0; i < 10000; i++ { _, err = store.Save(&Event{ - Timestamp: time.Now(), + Timestamp: time.Now().Add(-1 * time.Minute), Activity: PeerAddedByUser, - InitiatorID: "user_" + fmt.Sprint(i), - TargetID: "peer_" + fmt.Sprint(i), - AccountID: accountID, + InitiatorID: "google-oauth2|110866222733584764488", + TargetID: "100.101.249.29", + AccountID: "cebi9h3lo1hkhn1qc7cg", }) if err != nil { t.Fatal(err) @@ -31,7 +29,7 @@ func TestNewSQLiteStore(t *testing.T) { } } - result, err := store.Get(accountID, 0, 10, false) + /*result, err := store.Get(accountID, 0, 10, false) if err != nil { t.Fatal(err) return @@ -47,5 +45,5 @@ func TestNewSQLiteStore(t *testing.T) { } assert.Len(t, result, 5) - assert.True(t, result[0].Timestamp.After(result[len(result)-1].Timestamp)) + assert.True(t, result[0].Timestamp.After(result[len(result)-1].Timestamp))*/ } diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index f85a3c236..f0d66f641 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 3bdba4be5..238a74f49 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -519,13 +519,18 @@ components: activity_code: description: The string code of the activity that occurred during the event type: string - enum: [ "account.create", "user.join", "user.invite", "user.peer.add", "setupkey.peer.add" ] + enum: [ "account.create", "user.join", "user.invite", "user.peer.add", "setupkey.peer.add", "user.peer.delete", "rule.add", "rule.delete", "rule.update"] 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 @@ -533,6 +538,7 @@ components: - activity_code - initiator_id - target_id + - meta responses: not_found: description: Resource not found diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 99f929a6a..3c99a7948 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -14,10 +14,14 @@ const ( // Defines values for EventActivityCode. const ( EventActivityCodeAccountCreate EventActivityCode = "account.create" + EventActivityCodeRuleAdd EventActivityCode = "rule.add" + EventActivityCodeRuleDelete EventActivityCode = "rule.delete" + EventActivityCodeRuleUpdate EventActivityCode = "rule.update" EventActivityCodeSetupkeyPeerAdd EventActivityCode = "setupkey.peer.add" EventActivityCodeUserInvite EventActivityCode = "user.invite" EventActivityCodeUserJoin EventActivityCode = "user.join" EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add" + EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete" ) // Defines values for GroupPatchOperationOp. @@ -120,6 +124,9 @@ type Event struct { // 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"` diff --git a/management/server/http/events.go b/management/server/http/events.go index af332af4d..a635f1c3d 100644 --- a/management/server/http/events.go +++ b/management/server/http/events.go @@ -51,7 +51,12 @@ func (h *Events) GetEvents(w http.ResponseWriter, r *http.Request) { } 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, @@ -59,5 +64,6 @@ func toEventResponse(event *activity.Event) *api.Event { ActivityCode: api.EventActivityCode(event.Activity.StringCode()), TargetId: event.TargetID, Timestamp: event.Timestamp, + Meta: meta, } } diff --git a/management/server/http/peers.go b/management/server/http/peers.go index 045efdb92..0962b8b7d 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 9f1219185..42f9a25b2 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/peer.go b/management/server/peer.go index 268ba0d16..8793d4301 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -1,6 +1,7 @@ package server import ( + "fmt" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" @@ -74,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, "dns": p.FQDN(dnsDomain), "ip": p.IP} +} + // Copy PeerStatus func (p *PeerStatus) Copy() *PeerStatus { return &PeerStatus{ @@ -217,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() @@ -263,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 } @@ -449,6 +475,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* } opEvent.TargetID = newPeer.IP.String() + opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain()) _, err = am.eventStore.Save(opEvent) if err != nil { return nil, err diff --git a/management/server/rule.go b/management/server/rule.go index 98c74c02c..6111a05ce 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) }