From 0f68edbefb8dca10c37eed5fccae0d74627c8ec6 Mon Sep 17 00:00:00 2001 From: braginini Date: Mon, 5 Dec 2022 15:42:23 +0100 Subject: [PATCH 01/30] Initial event store draft --- go.mod | 1 + go.sum | 2 + management/server/audit/event.go | 32 +++++++++++++++ management/server/audit/sqlite.go | 66 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 management/server/audit/event.go create mode 100644 management/server/audit/sqlite.go diff --git a/go.mod b/go.mod index 2802a369b..ff2f3415b 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/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.13.0 diff --git a/go.sum b/go.sum index 707d7c808..65e1cdfc3 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/server/audit/event.go b/management/server/audit/event.go new file mode 100644 index 000000000..4599fc425 --- /dev/null +++ b/management/server/audit/event.go @@ -0,0 +1,32 @@ +package audit + +import "time" + +const ( + // DeviceEvent describes an event that happened of a device (e.g, connected/disconnected) + DeviceEvent EventType = "device" + // ManagementEvent describes an event that happened on a Management service (e.g., user added) + ManagementEvent EventType = "management" +) + +type EventType string + +// EventSink provides an interface to store or stream events. +type EventSink interface { + // Add an event to the sink. + Add(event *Event) error + // Close the sink flushing events if necessary + Close() error +} + +// Event represents a network activity. +type Event struct { + // Timestamp of the event + Timestamp time.Time + // Message of the event + Message string + // ID of the event (can be empty, meaning that it wasn't yet generated) + ID string + // Type of the event + Type EventType +} diff --git a/management/server/audit/sqlite.go b/management/server/audit/sqlite.go new file mode 100644 index 000000000..a59c642de --- /dev/null +++ b/management/server/audit/sqlite.go @@ -0,0 +1,66 @@ +package audit + +import ( + "database/sql" + "fmt" + _ "github.com/mattn/go-sqlite3" +) + +const ( + SQLiteEventSinkDB = "events.db" + createTableQuery = "CREATE TABLE IF NOT EXISTS events " + + "(id UNSIGNED BIG INT PRIMARY KEY AUTOINCREMENT, message TEXT, timestamp DATETIME);" +) + +type SQLiteSink struct { + db *sql.DB +} + +// NewSQLiteSink creates a new SQLiteSink with an event table if not exists. +func NewSQLiteSink(dbPath string) (*SQLiteSink, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, err + } + + _, err = db.Exec(createTableQuery) + if err != nil { + return nil, err + } + + return &SQLiteSink{db: db}, nil +} + +// Add an event to the SQLite table +func (sink *SQLiteSink) Add(event *Event) error { + + _, err := sink.db.Exec(createTableQuery) + if err != nil { + return err + } + + stmt, err := sink.db.Prepare("INSERT INTO events(message, timestamp) values(?, ?)") + if err != nil { + return err + } + + result, err := stmt.Exec(event.Message, event.Timestamp) + if err != nil { + if err == sql.ErrNoRows { + // Handle the case of no rows returned. + } + return album, err + } + + fmt.Println(result) + + return nil +} + +// Close the SQLiteSink +func (sink *SQLiteSink) Close() error { + if sink.db != nil { + return sink.db.Close() + } + return nil +} From 16e4d109ff7a92ca630cf91cdfb43a4974a17528 Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 6 Dec 2022 13:33:10 +0100 Subject: [PATCH 02/30] Rename audit to event --- management/server/{audit => event}/event.go | 21 +++++++++++--------- management/server/{audit => event}/sqlite.go | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) rename management/server/{audit => event}/event.go (66%) rename management/server/{audit => event}/sqlite.go (98%) diff --git a/management/server/audit/event.go b/management/server/event/event.go similarity index 66% rename from management/server/audit/event.go rename to management/server/event/event.go index 4599fc425..b1945abf4 100644 --- a/management/server/audit/event.go +++ b/management/server/event/event.go @@ -1,32 +1,35 @@ -package audit +package event import "time" const ( // DeviceEvent describes an event that happened of a device (e.g, connected/disconnected) - DeviceEvent EventType = "device" + DeviceEvent Type = "device" // ManagementEvent describes an event that happened on a Management service (e.g., user added) - ManagementEvent EventType = "management" + ManagementEvent Type = "management" ) -type EventType string +type Type string -// EventSink provides an interface to store or stream events. -type EventSink interface { +// Sink provides an interface to store or stream events. +type Sink interface { // Add an event to the sink. Add(event *Event) error // Close the sink flushing events if necessary Close() error } -// Event represents a network activity. +// Event represents a network activity event. type Event struct { // Timestamp of the event Timestamp time.Time // Message of the event Message string // ID of the event (can be empty, meaning that it wasn't yet generated) - ID string + ID uint64 // Type of the event - Type EventType + Type Type + + // + } diff --git a/management/server/audit/sqlite.go b/management/server/event/sqlite.go similarity index 98% rename from management/server/audit/sqlite.go rename to management/server/event/sqlite.go index a59c642de..7617ca55e 100644 --- a/management/server/audit/sqlite.go +++ b/management/server/event/sqlite.go @@ -1,4 +1,4 @@ -package audit +package event import ( "database/sql" From 65afc9a884fa526f8dc5a5bb3a426e7dca54f4af Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 6 Dec 2022 16:08:21 +0100 Subject: [PATCH 03/30] Add basic Save and Get operations to the event store --- management/server/event/event.go | 40 ++++++--- management/server/event/sqlite.go | 115 ++++++++++++++++++++----- management/server/event/sqlite_test.go | 47 ++++++++++ 3 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 management/server/event/sqlite_test.go diff --git a/management/server/event/event.go b/management/server/event/event.go index b1945abf4..dd80eadf8 100644 --- a/management/server/event/event.go +++ b/management/server/event/event.go @@ -11,25 +11,45 @@ const ( type Type string -// Sink provides an interface to store or stream events. -type Sink interface { - // Add an event to the sink. - Add(event *Event) error +// Store provides an interface to store or stream events. +type Store interface { + // Save an event in the store + Save(event Event) (*Event, error) + // GetSince returns a list of events from the store for a given account since the specified time + GetSince(accountID string, from time.Time) ([]Event, error) + // GetLast returns a top N of events from the store for a given account (ordered by timestamp desc) + GetLast(accountID string, limit int) ([]Event, error) // Close the sink flushing events if necessary Close() error } -// Event represents a network activity event. +// Event represents a network/system activity event. type Event struct { // Timestamp of the event Timestamp time.Time - // Message of the event - Message string + // Operation that was performed during the event + Operation string // ID of the event (can be empty, meaning that it wasn't yet generated) ID uint64 // Type of the event Type Type - - // - + // ModifierID is the ID of an object that modifies a Target + ModifierID string + // TargetID is the ID of an object that a Modifier modifies + TargetID string + // AccountID where event happened + AccountID string +} + +// Copy the event +func (e *Event) Copy() *Event { + return &Event{ + Timestamp: e.Timestamp, + Operation: e.Operation, + ID: e.ID, + Type: e.Type, + ModifierID: e.ModifierID, + TargetID: e.TargetID, + AccountID: e.AccountID, + } } diff --git a/management/server/event/sqlite.go b/management/server/event/sqlite.go index 7617ca55e..3eda3c994 100644 --- a/management/server/event/sqlite.go +++ b/management/server/event/sqlite.go @@ -2,23 +2,31 @@ package event import ( "database/sql" - "fmt" _ "github.com/mattn/go-sqlite3" + "path/filepath" + "time" ) const ( SQLiteEventSinkDB = "events.db" createTableQuery = "CREATE TABLE IF NOT EXISTS events " + - "(id UNSIGNED BIG INT PRIMARY KEY AUTOINCREMENT, message TEXT, timestamp DATETIME);" + "(id INTEGER PRIMARY KEY AUTOINCREMENT, account TEXT NOT NULL, " + + "operation TEXT, " + + "type TEXT, " + + "timestamp DATETIME, " + + "modifier TEXT," + + " target TEXT);" ) -type SQLiteSink struct { +// SQLiteStore is the implementation of the event.Store interface backed by SQLite +type SQLiteStore struct { db *sql.DB } -// NewSQLiteSink creates a new SQLiteSink with an event table if not exists. -func NewSQLiteSink(dbPath string) (*SQLiteSink, error) { - db, err := sql.Open("sqlite3", dbPath) +// NewSQLiteStore creates a new SQLiteStore with an event table if not exists. +func NewSQLiteStore(dataDir string) (*SQLiteStore, error) { + dbFile := filepath.Join(dataDir, SQLiteEventSinkDB) + db, err := sql.Open("sqlite3", dbFile) if err != nil { return nil, err } @@ -28,39 +36,102 @@ func NewSQLiteSink(dbPath string) (*SQLiteSink, error) { return nil, err } - return &SQLiteSink{db: db}, nil + return &SQLiteStore{db: db}, nil } -// Add an event to the SQLite table -func (sink *SQLiteSink) Add(event *Event) error { +func processResult(result *sql.Rows) ([]Event, error) { + events := make([]Event, 0) + for result.Next() { + var id int64 + var operation string + var timestamp time.Time + var modifier string + var target string + var account string + var typ Type + err := result.Scan(&id, &operation, ×tamp, &modifier, &target, &account, &typ) + if err != nil { + return nil, err + } - _, err := sink.db.Exec(createTableQuery) - if err != nil { - return err + events = append(events, Event{ + Timestamp: timestamp, + Operation: operation, + ID: uint64(id), + Type: typ, + ModifierID: modifier, + TargetID: target, + AccountID: account, + }) } - stmt, err := sink.db.Prepare("INSERT INTO events(message, timestamp) values(?, ?)") + return events, nil +} + +// GetLast returns a top N of events from the store for a given account (ordered by timestamp desc) +func (store *SQLiteStore) GetLast(accountID string, limit int) ([]Event, error) { + stmt, err := store.db.Prepare("SELECT id, operation, timestamp, modifier, target, account, type" + + " FROM events WHERE account = ? ORDER BY timestamp DESC limit ?;") if err != nil { - return err + return nil, err } - result, err := stmt.Exec(event.Message, event.Timestamp) + result, err := stmt.Query(accountID, limit) + if err != nil { + return nil, err + } + + defer result.Close() //nolint + return processResult(result) +} + +// GetSince returns a list of events from the store for a given account since the specified time +func (store *SQLiteStore) GetSince(accountID string, from time.Time) ([]Event, error) { + stmt, err := store.db.Prepare("SELECT id, operation, timestamp, modifier, target, account, type" + + " FROM events WHERE account = ? and timestamp >= ?;") + if err != nil { + return nil, err + } + + result, err := stmt.Query(accountID, from) + if err != nil { + return nil, err + } + + defer result.Close() //nolint + return processResult(result) +} + +// Save an event in the SQLite events table +func (store *SQLiteStore) Save(event Event) (*Event, error) { + + stmt, err := store.db.Prepare("INSERT INTO events(operation, timestamp, modifier, target, account, type) VALUES(?, ?, ?, ?, ?, ?)") + if err != nil { + return nil, err + } + + result, err := stmt.Exec(event.Operation, event.Timestamp, event.ModifierID, event.TargetID, event.AccountID, event.Type) if err != nil { if err == sql.ErrNoRows { // Handle the case of no rows returned. } - return album, err + return nil, err } - fmt.Println(result) + id, err := result.LastInsertId() + if err != nil { + return nil, err + } - return nil + eventCopy := event.Copy() + eventCopy.ID = uint64(id) + return eventCopy, nil } -// Close the SQLiteSink -func (sink *SQLiteSink) Close() error { - if sink.db != nil { - return sink.db.Close() +// Close the SQLiteStore +func (store *SQLiteStore) Close() error { + if store.db != nil { + return store.db.Close() } return nil } diff --git a/management/server/event/sqlite_test.go b/management/server/event/sqlite_test.go new file mode 100644 index 000000000..9c2400c4a --- /dev/null +++ b/management/server/event/sqlite_test.go @@ -0,0 +1,47 @@ +package event + +import ( + "fmt" + "testing" + "time" +) + +func TestNewSQLiteStore(t *testing.T) { + dataDir := t.TempDir() + store, err := NewSQLiteStore(dataDir) + if err != nil { + t.Fatal(err) + return + } + + accountID := "account_1" + eventTime := time.Now() + _, err = store.Save(Event{ + Timestamp: eventTime, + Operation: "cool operation", + Type: ManagementEvent, + ModifierID: "user_1", + TargetID: "peer_1", + AccountID: accountID, + }) + if err != nil { + t.Fatal(err) + return + } + + result, err := store.GetSince(accountID, eventTime.Add(-10*time.Second)) + if err != nil { + t.Fatal(err) + return + } + + fmt.Println(result) + + result, err = store.GetLast(accountID, 10) + if err != nil { + t.Fatal(err) + return + } + + fmt.Println(result) +} From 1c1e639445ed05c607b436158dc1b14252d3fd39 Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 6 Dec 2022 16:17:01 +0100 Subject: [PATCH 04/30] Add basic Save and Get operations to the event store --- management/server/event/sqlite_test.go | 40 ++++++++++++++------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/management/server/event/sqlite_test.go b/management/server/event/sqlite_test.go index 9c2400c4a..1cd0675f1 100644 --- a/management/server/event/sqlite_test.go +++ b/management/server/event/sqlite_test.go @@ -15,33 +15,35 @@ func TestNewSQLiteStore(t *testing.T) { } accountID := "account_1" - eventTime := time.Now() - _, err = store.Save(Event{ - Timestamp: eventTime, - Operation: "cool operation", - Type: ManagementEvent, - ModifierID: "user_1", - TargetID: "peer_1", - AccountID: accountID, - }) + + for i := 0; i < 10; i++ { + _, err = store.Save(Event{ + Timestamp: time.Now(), + Operation: "cool_operation_" + fmt.Sprint(i), + Type: ManagementEvent, + ModifierID: "user_" + fmt.Sprint(i), + TargetID: "peer_" + fmt.Sprint(i), + AccountID: accountID, + }) + if err != nil { + t.Fatal(err) + return + } + } + + result, err := store.GetSince(accountID, time.Now().Add(-30*time.Second)) if err != nil { t.Fatal(err) return } - result, err := store.GetSince(accountID, eventTime.Add(-10*time.Second)) + fmt.Println(len(result)) + + result, err = store.GetLast(accountID, 5) if err != nil { t.Fatal(err) return } - fmt.Println(result) - - result, err = store.GetLast(accountID, 10) - if err != nil { - t.Fatal(err) - return - } - - fmt.Println(result) + fmt.Println(len(result)) } From d98a8e0645b0b6a3ae3804fe56cb4d16e01cd2f8 Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 6 Dec 2022 16:58:08 +0100 Subject: [PATCH 05/30] Fix lint issue --- management/server/event/sqlite.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/management/server/event/sqlite.go b/management/server/event/sqlite.go index 3eda3c994..7dd8887d9 100644 --- a/management/server/event/sqlite.go +++ b/management/server/event/sqlite.go @@ -112,9 +112,6 @@ func (store *SQLiteStore) Save(event Event) (*Event, error) { result, err := stmt.Exec(event.Operation, event.Timestamp, event.ModifierID, event.TargetID, event.AccountID, event.Type) if err != nil { - if err == sql.ErrNoRows { - // Handle the case of no rows returned. - } return nil, err } From ef97e0840c5f6bd2c07c42212e659637aa481148 Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 6 Dec 2022 16:59:35 +0100 Subject: [PATCH 06/30] Fix SQLiteStore test --- management/server/event/sqlite_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/management/server/event/sqlite_test.go b/management/server/event/sqlite_test.go index 1cd0675f1..66e6d33a7 100644 --- a/management/server/event/sqlite_test.go +++ b/management/server/event/sqlite_test.go @@ -2,6 +2,7 @@ package event import ( "fmt" + "github.com/stretchr/testify/assert" "testing" "time" ) @@ -37,7 +38,7 @@ func TestNewSQLiteStore(t *testing.T) { return } - fmt.Println(len(result)) + assert.Len(t, result, 10) result, err = store.GetLast(accountID, 5) if err != nil { @@ -45,5 +46,5 @@ func TestNewSQLiteStore(t *testing.T) { return } - fmt.Println(len(result)) + assert.Len(t, result, 5) } From 0f4d132b935c0d2b837aa74229f607ac96d5c0ab Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 7 Dec 2022 10:30:59 +0100 Subject: [PATCH 07/30] Fetch events with offset limit --- management/server/event/event.go | 5 +++-- management/server/event/sqlite.go | 15 ++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/management/server/event/event.go b/management/server/event/event.go index dd80eadf8..b5b052667 100644 --- a/management/server/event/event.go +++ b/management/server/event/event.go @@ -9,6 +9,7 @@ const ( ManagementEvent Type = "management" ) +// Type of the Event type Type string // Store provides an interface to store or stream events. @@ -17,8 +18,8 @@ type Store interface { Save(event Event) (*Event, error) // GetSince returns a list of events from the store for a given account since the specified time GetSince(accountID string, from time.Time) ([]Event, error) - // GetLast returns a top N of events from the store for a given account (ordered by timestamp desc) - GetLast(accountID string, limit int) ([]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 } diff --git a/management/server/event/sqlite.go b/management/server/event/sqlite.go index 7dd8887d9..de0d91d30 100644 --- a/management/server/event/sqlite.go +++ b/management/server/event/sqlite.go @@ -2,6 +2,7 @@ package event import ( "database/sql" + "fmt" _ "github.com/mattn/go-sqlite3" "path/filepath" "time" @@ -68,15 +69,19 @@ func processResult(result *sql.Rows) ([]Event, error) { return events, nil } -// GetLast returns a top N of events from the store for a given account (ordered by timestamp desc) -func (store *SQLiteStore) GetLast(accountID string, limit int) ([]Event, error) { - stmt, err := store.db.Prepare("SELECT id, operation, timestamp, modifier, target, account, type" + - " FROM events WHERE account = ? ORDER BY timestamp DESC limit ?;") +// Get returns "limit" number of events from index ordered descending or ascending by a timestamp +func (store *SQLiteStore) Get(accountID string, offset, limit int, descending bool) ([]Event, error) { + order := "DESC" + if !descending { + order = "ASC" + } + stmt, err := store.db.Prepare(fmt.Sprintf("SELECT id, operation, timestamp, modifier, target, account, type"+ + " FROM events WHERE account = ? ORDER BY timestamp %s LIMIT ? OFFSET ?;", order)) if err != nil { return nil, err } - result, err := stmt.Query(accountID, limit) + result, err := stmt.Query(accountID, limit, offset) if err != nil { return nil, err } From 0e3bbf0e557513521704670401ab5ad7cbf9dbf9 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 7 Dec 2022 10:31:05 +0100 Subject: [PATCH 08/30] Fetch events with offset limit --- management/server/event/sqlite_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/event/sqlite_test.go b/management/server/event/sqlite_test.go index 66e6d33a7..9aae112a9 100644 --- a/management/server/event/sqlite_test.go +++ b/management/server/event/sqlite_test.go @@ -40,7 +40,7 @@ func TestNewSQLiteStore(t *testing.T) { assert.Len(t, result, 10) - result, err = store.GetLast(accountID, 5) + result, err = store.Get(accountID, 1, 5, true) if err != nil { t.Fatal(err) return From c09c3a70ab24368cf1ac67c14ce9b2cd4d51ff62 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 7 Dec 2022 10:32:38 +0100 Subject: [PATCH 09/30] Remove unused event store methods --- management/server/event/event.go | 2 -- management/server/event/sqlite.go | 17 ----------------- 2 files changed, 19 deletions(-) diff --git a/management/server/event/event.go b/management/server/event/event.go index b5b052667..26cfbf531 100644 --- a/management/server/event/event.go +++ b/management/server/event/event.go @@ -16,8 +16,6 @@ type Type string type Store interface { // Save an event in the store Save(event Event) (*Event, error) - // GetSince returns a list of events from the store for a given account since the specified time - GetSince(accountID string, from time.Time) ([]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 diff --git a/management/server/event/sqlite.go b/management/server/event/sqlite.go index de0d91d30..b1a86d3c2 100644 --- a/management/server/event/sqlite.go +++ b/management/server/event/sqlite.go @@ -90,23 +90,6 @@ func (store *SQLiteStore) Get(accountID string, offset, limit int, descending bo return processResult(result) } -// GetSince returns a list of events from the store for a given account since the specified time -func (store *SQLiteStore) GetSince(accountID string, from time.Time) ([]Event, error) { - stmt, err := store.db.Prepare("SELECT id, operation, timestamp, modifier, target, account, type" + - " FROM events WHERE account = ? and timestamp >= ?;") - if err != nil { - return nil, err - } - - result, err := stmt.Query(accountID, from) - if err != nil { - return nil, err - } - - defer result.Close() //nolint - return processResult(result) -} - // Save an event in the SQLite events table func (store *SQLiteStore) Save(event Event) (*Event, error) { From e81f4af3e8986d42a5d356223aba6f724df35dd1 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 7 Dec 2022 10:33:32 +0100 Subject: [PATCH 10/30] Remove unused event store methods --- management/server/event/sqlite_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/management/server/event/sqlite_test.go b/management/server/event/sqlite_test.go index 9aae112a9..fb31700ea 100644 --- a/management/server/event/sqlite_test.go +++ b/management/server/event/sqlite_test.go @@ -32,7 +32,7 @@ func TestNewSQLiteStore(t *testing.T) { } } - result, err := store.GetSince(accountID, time.Now().Add(-30*time.Second)) + result, err := store.Get(accountID, 0, 10, true) if err != nil { t.Fatal(err) return @@ -40,7 +40,7 @@ func TestNewSQLiteStore(t *testing.T) { assert.Len(t, result, 10) - result, err = store.Get(accountID, 1, 5, true) + result, err = store.Get(accountID, 0, 5, true) if err != nil { t.Fatal(err) return From e75c25b10d3859cbc4a416ad9155d115d9935b44 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 7 Dec 2022 10:37:49 +0100 Subject: [PATCH 11/30] Test event ordering in SQLite storage --- management/server/event/sqlite_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/management/server/event/sqlite_test.go b/management/server/event/sqlite_test.go index fb31700ea..362913651 100644 --- a/management/server/event/sqlite_test.go +++ b/management/server/event/sqlite_test.go @@ -32,13 +32,14 @@ func TestNewSQLiteStore(t *testing.T) { } } - result, err := store.Get(accountID, 0, 10, true) + 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 { @@ -47,4 +48,5 @@ func TestNewSQLiteStore(t *testing.T) { } assert.Len(t, result, 5) + assert.True(t, result[0].Timestamp.After(result[len(result)-1].Timestamp)) } From 04ea3000436a8fda100c1d1819460187cd5e473d Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 7 Dec 2022 11:06:46 +0100 Subject: [PATCH 12/30] Add peer operation --- management/cmd/management.go | 8 +++++++- management/server/account.go | 5 ++++- management/server/event/event.go | 15 +++++++++++++++ management/server/event/sqlite.go | 4 ++-- management/server/event/sqlite_test.go | 12 ++++++------ management/server/peer.go | 19 +++++++++++++++++++ 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/management/cmd/management.go b/management/cmd/management.go index 82168ec72..93b487c63 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/event" 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 := event.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 2d434e2d9..1df30dbf0 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/event" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/status" @@ -99,6 +100,7 @@ type DefaultAccountManager struct { idpManager idp.Manager cacheManager cache.CacheInterface[[]*idp.UserData] ctx context.Context + eventStore event.Store // singleAccountMode indicates whether the instance has a single account. // If true, then every new user will end up under the same account. @@ -390,7 +392,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 event.Store) (*DefaultAccountManager, error) { am := &DefaultAccountManager{ Store: store, peersUpdateManager: peersUpdateManager, @@ -399,6 +401,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 diff --git a/management/server/event/event.go b/management/server/event/event.go index 26cfbf531..925633398 100644 --- a/management/server/event/event.go +++ b/management/server/event/event.go @@ -9,9 +9,22 @@ const ( ManagementEvent Type = "management" ) +const ( + AddPeerByUserOperation Operation = iota + AddPeerWithKeyOperation +) + +const ( + AddPeerByUserOperationMessage string = "Add new peer" + AddPeerWithKeyOperationMessage string = AddPeerByUserOperationMessage +) + // Type of the Event type Type string +// Operation is an action that triggered an Event +type Operation int + // Store provides an interface to store or stream events. type Store interface { // Save an event in the store @@ -28,6 +41,8 @@ type Event struct { Timestamp time.Time // Operation that was performed during the event Operation string + // OperationCode that was performed during the event + OperationCode Operation // ID of the event (can be empty, meaning that it wasn't yet generated) ID uint64 // Type of the event diff --git a/management/server/event/sqlite.go b/management/server/event/sqlite.go index b1a86d3c2..266f60fa7 100644 --- a/management/server/event/sqlite.go +++ b/management/server/event/sqlite.go @@ -12,7 +12,7 @@ const ( SQLiteEventSinkDB = "events.db" createTableQuery = "CREATE TABLE IF NOT EXISTS events " + "(id INTEGER PRIMARY KEY AUTOINCREMENT, account TEXT NOT NULL, " + - "operation TEXT, " + + "operation INTEGER, " + "type TEXT, " + "timestamp DATETIME, " + "modifier TEXT," + @@ -98,7 +98,7 @@ func (store *SQLiteStore) Save(event Event) (*Event, error) { return nil, err } - result, err := stmt.Exec(event.Operation, event.Timestamp, event.ModifierID, event.TargetID, event.AccountID, event.Type) + result, err := stmt.Exec(event.OperationCode, event.Timestamp, event.ModifierID, event.TargetID, event.AccountID, event.Type) if err != nil { return nil, err } diff --git a/management/server/event/sqlite_test.go b/management/server/event/sqlite_test.go index 362913651..ae91022b5 100644 --- a/management/server/event/sqlite_test.go +++ b/management/server/event/sqlite_test.go @@ -19,12 +19,12 @@ func TestNewSQLiteStore(t *testing.T) { for i := 0; i < 10; i++ { _, err = store.Save(Event{ - Timestamp: time.Now(), - Operation: "cool_operation_" + fmt.Sprint(i), - Type: ManagementEvent, - ModifierID: "user_" + fmt.Sprint(i), - TargetID: "peer_" + fmt.Sprint(i), - AccountID: accountID, + Timestamp: time.Now(), + OperationCode: AddPeerByUserOperation, + Type: ManagementEvent, + ModifierID: "user_" + fmt.Sprint(i), + TargetID: "peer_" + fmt.Sprint(i), + AccountID: accountID, }) if err != nil { t.Fatal(err) diff --git a/management/server/peer.go b/management/server/peer.go index 01e7ffed4..9c3d41f53 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -2,6 +2,7 @@ package server import ( nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/event" "github.com/netbirdio/netbird/management/server/status" "net" "strings" @@ -340,6 +341,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* var account *Account var err error addedByUser := false + if len(userID) > 0 { addedByUser = true account, err = am.Store.GetAccountByUser(userID) @@ -359,6 +361,12 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* return nil, err } + opEvent := event.Event{ + Timestamp: time.Now(), + Type: event.ManagementEvent, + AccountID: account.Id, + } + if !addedByUser { // validate the setup key if adding with a key sk, err := account.FindSetupKey(upperKey) @@ -371,6 +379,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* } account.SetupKeys[sk.Key] = sk.IncrementUsage() + opEvent.ModifierID = sk.Id + opEvent.OperationCode = event.AddPeerWithKeyOperation + } else { + opEvent.ModifierID = userID + opEvent.OperationCode = event.AddPeerByUserOperation } takenIps := account.getTakenIPs() @@ -436,6 +449,12 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* return nil, err } + opEvent.TargetID = newPeer.IP.String() + _, err = am.eventStore.Save(opEvent) + if err != nil { + return nil, err + } + return newPeer, nil } From e741775d4d9774a2e99bb7846b0a009c39aebeeb Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 7 Dec 2022 12:04:43 +0100 Subject: [PATCH 13/30] Add join user operation --- management/server/account.go | 14 ++++++++++++++ management/server/event/event.go | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/management/server/account.go b/management/server/account.go index 1df30dbf0..968a8f0fa 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -752,6 +752,20 @@ func (am *DefaultAccountManager) handleNewUserAccount(domainAcc *Account, claims return nil, err } + opEvent := event.Event{ + Timestamp: time.Now(), + Type: event.ManagementEvent, + OperationCode: event.UserJoinedOperation, + AccountID: account.Id, + TargetID: claims.UserId, + ModifierID: claims.UserId, + } + + _, err = am.eventStore.Save(opEvent) + if err != nil { + return nil, err + } + return account, nil } diff --git a/management/server/event/event.go b/management/server/event/event.go index 925633398..e3a650f05 100644 --- a/management/server/event/event.go +++ b/management/server/event/event.go @@ -12,13 +12,29 @@ const ( const ( AddPeerByUserOperation Operation = iota AddPeerWithKeyOperation + UserJoinedOperation ) const ( AddPeerByUserOperationMessage string = "Add new peer" AddPeerWithKeyOperationMessage string = AddPeerByUserOperationMessage + UserJoinedOperationMessage string = "New user joined" ) +// MessageForOperation returns a string message for an Operation +func MessageForOperation(op Operation) string { + switch op { + case AddPeerByUserOperation: + return AddPeerByUserOperationMessage + case AddPeerWithKeyOperation: + return AddPeerWithKeyOperationMessage + case UserJoinedOperation: + return UserJoinedOperationMessage + default: + return "UNKNOWN_OPERATION" + } +} + // Type of the Event type Type string From a212686193e6cd9c6354f68fb5eaa9050364be77 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 6 Dec 2022 10:11:57 +0100 Subject: [PATCH 14/30] Add network routes distribution groups (#606) Updated tests, API, and account manager methods Sync routes to peers in the distribution groups Added store upgrade by adding the All group to routes that don't have them --- .github/workflows/golang-test-linux.yml | 4 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/release.yml | 2 +- management/server/account.go | 61 +++-- management/server/account_test.go | 10 +- management/server/dns.go | 10 +- management/server/file_store.go | 13 + management/server/http/api/openapi.yml | 8 +- management/server/http/api/types.gen.go | 7 + management/server/http/routes.go | 14 +- management/server/http/routes_test.go | 29 +- management/server/mock_server/account_mock.go | 6 +- management/server/peer.go | 18 +- management/server/route.go | 23 +- management/server/route_test.go | 255 +++++++++++++++--- route/route.go | 25 +- 16 files changed, 386 insertions(+), 101 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 64fe743a9..dbb31ccdb 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v2 - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-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 - name: Install modules run: go mod tidy @@ -60,7 +60,7 @@ jobs: uses: actions/checkout@v2 - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-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 - name: Install modules run: go mod tidy diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5b8f8412a..956d37936 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -11,7 +11,7 @@ jobs: with: go-version: 1.19.x - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-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 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1da745947..32dd4d02c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,7 +104,7 @@ jobs: run: git --no-pager diff --exit-code - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64 + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64 - name: Install rsrc run: go install github.com/akavel/rsrc@v0.10.2 - name: Generate windows rsrc diff --git a/management/server/account.go b/management/server/account.go index 968a8f0fa..3eff78ad5 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -76,7 +76,7 @@ type AccountManager interface { DeleteRule(accountId, ruleID 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, enabled bool) (*route.Route, error) + CreateRoute(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) SaveRoute(accountID string, route *route.Route) error UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error) DeleteRoute(accountID, routeID string) error @@ -139,31 +139,41 @@ type UserInfo struct { Status string `json:"-"` } -// GetPeersRoutes returns all active routes of provided peers -func (a *Account) GetPeersRoutes(givenPeers []*Peer) []*route.Route { - //TODO Peer.ID migration: we will need to replace search by Peer.ID here - routes := make([]*route.Route, 0) - for _, peer := range givenPeers { - peerRoutes := a.GetPeerRoutes(peer.Key) - activeRoutes := make([]*route.Route, 0) - for _, pr := range peerRoutes { - if pr.Enabled { - activeRoutes = append(activeRoutes, pr) - } - } - if len(activeRoutes) > 0 { - routes = append(routes, activeRoutes...) - } +// getRoutesToSync returns the enabled routes for the peer ID and the routes +// from the ACL peers that have distribution groups associated with the peer ID +func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Route { + routes := a.getEnabledRoutesByPeer(peerID) + groupListMap := a.getPeerGroups(peerID) + for _, peer := range aclPeers { + activeRoutes := a.getEnabledRoutesByPeer(peer.Key) + filteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap) + routes = append(routes, filteredRoutes...) } + return routes } -// GetPeerRoutes returns a list of routes of a given peer -func (a *Account) GetPeerRoutes(peerPubKey string) []*route.Route { +// filterRoutesByGroups returns a list with routes that have distribution groups in the group's map +func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap lookupMap) []*route.Route { + var filteredRoutes []*route.Route + for _, r := range routes { + for _, groupID := range r.Groups { + _, found := groupListMap[groupID] + if found { + filteredRoutes = append(filteredRoutes, r) + break + } + } + } + return filteredRoutes +} + +// getEnabledRoutesByPeer returns a list of routes of a given peer +func (a *Account) getEnabledRoutesByPeer(peerPubKey string) []*route.Route { //TODO Peer.ID migration: we will need to replace search by Peer.ID here var routes []*route.Route for _, r := range a.Routes { - if r.Peer == peerPubKey { + if r.Peer == peerPubKey && r.Enabled { routes = append(routes, r) continue } @@ -301,6 +311,19 @@ func (a *Account) getUserGroups(userID string) ([]string, error) { return user.AutoGroups, nil } +func (a *Account) getPeerGroups(peerID string) lookupMap { + groupList := make(lookupMap) + for groupID, group := range a.Groups { + for _, id := range group.Peers { + if id == peerID { + groupList[groupID] = struct{}{} + break + } + } + } + return groupList +} + func (a *Account) getSetupKeyGroups(setupKey string) ([]string, error) { key, err := a.FindSetupKey(setupKey) if err != nil { diff --git a/management/server/account_test.go b/management/server/account_test.go index 28d1e991b..667d582be 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1075,7 +1075,7 @@ func TestFileStore_GetRoutesByPrefix(t *testing.T) { assert.Contains(t, routeIDs, "route-2") } -func TestAccount_GetPeersRoutes(t *testing.T) { +func TestAccount_GetRoutesToSync(t *testing.T) { _, prefix, err := route.ParseNetwork("192.168.64.0/24") if err != nil { t.Fatal(err) @@ -1084,6 +1084,7 @@ func TestAccount_GetPeersRoutes(t *testing.T) { Peers: map[string]*Peer{ "peer-1": {Key: "peer-1"}, "peer-2": {Key: "peer-2"}, "peer-3": {Key: "peer-1"}, }, + Groups: map[string]*Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}}, Routes: map[string]*route.Route{ "route-1": { ID: "route-1", @@ -1095,6 +1096,7 @@ func TestAccount_GetPeersRoutes(t *testing.T) { Masquerade: false, Metric: 999, Enabled: true, + Groups: []string{"group1"}, }, "route-2": { ID: "route-2", @@ -1106,11 +1108,12 @@ func TestAccount_GetPeersRoutes(t *testing.T) { Masquerade: false, Metric: 999, Enabled: true, + Groups: []string{"group1"}, }, }, } - routes := account.GetPeersRoutes([]*Peer{{Key: "peer-1"}, {Key: "peer-2"}, {Key: "non-existing-peer"}}) + routes := account.getRoutesToSync("peer-2", []*Peer{{Key: "peer-1"}, {Key: "peer-3"}}) assert.Len(t, routes, 2) routeIDs := make(map[string]struct{}, 2) @@ -1120,6 +1123,9 @@ func TestAccount_GetPeersRoutes(t *testing.T) { assert.Contains(t, routeIDs, "route-1") assert.Contains(t, routeIDs, "route-2") + emptyRoutes := account.getRoutesToSync("peer-3", []*Peer{{Key: "peer-1"}, {Key: "peer-2"}}) + + assert.Len(t, emptyRoutes, 0) } func TestAccount_Copy(t *testing.T) { diff --git a/management/server/dns.go b/management/server/dns.go index 62a2e1274..85c4e481f 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -78,15 +78,7 @@ func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone { } func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup { - groupList := make(lookupMap) - for groupID, group := range account.Groups { - for _, id := range group.Peers { - if id == peerID { - groupList[groupID] = struct{}{} - break - } - } - } + groupList := account.getPeerGroups(peerID) var peerNSGroups []*nbdns.NameServerGroup diff --git a/management/server/file_store.go b/management/server/file_store.go index aab3eb8d8..cd66d4e5b 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -105,6 +105,19 @@ func restore(file string) (*FileStore, error) { if len(existingLabels) != len(account.Peers) { addPeerLabelsToAccount(account, existingLabels) } + + allGroup, err := account.GetGroupAll() + if err != nil { + log.Errorf("unable to find the All group, this should happen only when migrate from a version that didn't support groups. Error: %v", err) + // if the All group didn't exist we probably don't have routes to update + continue + } + + for _, route := range account.Routes { + if len(route.Groups) == 0 { + route.Groups = []string{allGroup.ID} + } + } } // we need this persist to apply changes we made to account.Peers (we set them to Disconnected) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 96a90053a..c25e14fa0 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -379,6 +379,11 @@ components: masquerade: description: Indicate if peer should masquerade traffic to this route's prefix type: boolean + groups: + description: Route group tag groups + type: array + items: + type: string required: - id - description @@ -388,6 +393,7 @@ components: - network - metric - masquerade + - groups Route: allOf: - type: object @@ -410,7 +416,7 @@ components: path: description: Route field to update in form / type: string - enum: [ "network","network_id","description","enabled","peer","metric","masquerade" ] + enum: [ "network","network_id","description","enabled","peer","metric","masquerade", "groups" ] required: - path Nameserver: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index c42a0e6b1..3ba8e6775 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -65,6 +65,7 @@ const ( const ( RoutePatchOperationPathDescription RoutePatchOperationPath = "description" RoutePatchOperationPathEnabled RoutePatchOperationPath = "enabled" + RoutePatchOperationPathGroups RoutePatchOperationPath = "groups" RoutePatchOperationPathMasquerade RoutePatchOperationPath = "masquerade" RoutePatchOperationPathMetric RoutePatchOperationPath = "metric" RoutePatchOperationPathNetwork RoutePatchOperationPath = "network" @@ -296,6 +297,9 @@ type Route struct { // Enabled Route status Enabled bool `json:"enabled"` + // Groups Route group tag groups + Groups []string `json:"groups"` + // Id Route Id Id string `json:"id"` @@ -344,6 +348,9 @@ type RouteRequest struct { // Enabled Route status Enabled bool `json:"enabled"` + // Groups Route group tag groups + Groups []string `json:"groups"` + // Masquerade Indicate if peer should masquerade traffic to this route's prefix Masquerade bool `json:"masquerade"` diff --git a/management/server/http/routes.go b/management/server/http/routes.go index f85e3ee20..8145ba0e1 100644 --- a/management/server/http/routes.go +++ b/management/server/http/routes.go @@ -89,7 +89,7 @@ func (h *Routes) CreateRouteHandler(w http.ResponseWriter, r *http.Request) { return } - newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), peerKey, req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Enabled) + newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), peerKey, req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Groups, req.Enabled) if err != nil { util.WriteError(err, w) return @@ -162,6 +162,7 @@ func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) { Metric: req.Metric, Description: req.Description, Enabled: req.Enabled, + Groups: req.Groups, } err = h.accountManager.SaveRoute(account.Id, newRoute) @@ -298,6 +299,16 @@ func (h *Routes) PatchRouteHandler(w http.ResponseWriter, r *http.Request) { Type: server.UpdateRouteEnabled, Values: patch.Value, }) + case api.RoutePatchOperationPathGroups: + if patch.Op != api.RoutePatchOperationOpReplace { + util.WriteError(status.Errorf(status.InvalidArgument, + "groups field only accepts replace operation, got %s", patch.Op), w) + return + } + operations = append(operations, server.RouteUpdateOperation{ + Type: server.UpdateRouteGroups, + Values: patch.Value, + }) default: util.WriteError(status.Errorf(status.InvalidArgument, "invalid patch path"), w) return @@ -383,5 +394,6 @@ func toRouteResponse(account *server.Account, serverRoute *route.Route) *api.Rou NetworkType: serverRoute.NetworkType.String(), Masquerade: serverRoute.Masquerade, Metric: serverRoute.Metric, + Groups: serverRoute.Groups, } } diff --git a/management/server/http/routes_test.go b/management/server/http/routes_test.go index cd3822585..9af5e7f55 100644 --- a/management/server/http/routes_test.go +++ b/management/server/http/routes_test.go @@ -28,6 +28,7 @@ const ( notFoundPeerID = "100.64.0.200" existingPeerKey = "existingPeerKey" testAccountID = "test_id" + existingGroupID = "testGroup" ) var baseExistingRoute = &route.Route{ @@ -39,6 +40,7 @@ var baseExistingRoute = &route.Route{ Metric: 9999, Masquerade: false, Enabled: true, + Groups: []string{existingGroupID}, } var testingAccount = &server.Account{ @@ -64,7 +66,7 @@ func initRoutesTestData() *Routes { } return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID) }, - CreateRouteFunc: func(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) { + CreateRouteFunc: func(accountID string, network, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) { networkType, p, _ := route.ParseNetwork(network) return &route.Route{ ID: existingRouteID, @@ -75,6 +77,7 @@ func initRoutesTestData() *Routes { Description: description, Masquerade: masquerade, Enabled: enabled, + Groups: groups, }, nil }, SaveRouteFunc: func(_ string, _ *route.Route) error { @@ -116,6 +119,8 @@ func initRoutesTestData() *Routes { routeToUpdate.Masquerade, _ = strconv.ParseBool(operation.Values[0]) case server.UpdateRouteEnabled: routeToUpdate.Enabled, _ = strconv.ParseBool(operation.Values[0]) + case server.UpdateRouteGroups: + routeToUpdate.Groups = operation.Values default: return nil, fmt.Errorf("no operation") } @@ -181,7 +186,7 @@ func TestRoutesHandlers(t *testing.T) { requestType: http.MethodPost, requestPath: "/api/routes", requestBody: bytes.NewBuffer( - []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID))), + []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID))), expectedStatus: http.StatusOK, expectedBody: true, expectedRoute: &api.Route{ @@ -193,13 +198,14 @@ func TestRoutesHandlers(t *testing.T) { NetworkType: route.IPv4NetworkString, Masquerade: false, Enabled: false, + Groups: []string{existingGroupID}, }, }, { name: "POST Not Found Peer", requestType: http.MethodPost, requestPath: "/api/routes", - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", notFoundPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", notFoundPeerID, existingGroupID)), expectedStatus: http.StatusNotFound, expectedBody: false, }, @@ -207,7 +213,7 @@ func TestRoutesHandlers(t *testing.T) { name: "POST Invalid Network Identifier", requestType: http.MethodPost, requestPath: "/api/routes", - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -215,7 +221,7 @@ func TestRoutesHandlers(t *testing.T) { name: "POST Invalid Network", requestType: http.MethodPost, requestPath: "/api/routes", - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -223,7 +229,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT OK", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusOK, expectedBody: true, expectedRoute: &api.Route{ @@ -235,13 +241,14 @@ func TestRoutesHandlers(t *testing.T) { NetworkType: route.IPv4NetworkString, Masquerade: false, Enabled: false, + Groups: []string{existingGroupID}, }, }, { name: "PUT Not Found Route", requestType: http.MethodPut, requestPath: "/api/routes/" + notFoundRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusNotFound, expectedBody: false, }, @@ -249,7 +256,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT Not Found Peer", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", notFoundPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", notFoundPeerID, existingGroupID)), expectedStatus: http.StatusNotFound, expectedBody: false, }, @@ -257,7 +264,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT Invalid Network Identifier", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -265,7 +272,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT Invalid Network", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -285,6 +292,7 @@ func TestRoutesHandlers(t *testing.T) { Masquerade: baseExistingRoute.Masquerade, Enabled: baseExistingRoute.Enabled, Metric: baseExistingRoute.Metric, + Groups: baseExistingRoute.Groups, }, }, { @@ -304,6 +312,7 @@ func TestRoutesHandlers(t *testing.T) { Masquerade: baseExistingRoute.Masquerade, Enabled: baseExistingRoute.Enabled, Metric: baseExistingRoute.Metric, + Groups: baseExistingRoute.Groups, }, }, { diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 3030b831d..8efeee33a 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -43,7 +43,7 @@ type MockAccountManager struct { UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) error UpdatePeerSSHKeyFunc func(peerKey string, sshKey string) error UpdatePeerFunc func(accountID string, peer *server.Peer) (*server.Peer, error) - CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) + CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) GetRouteFunc func(accountID, routeID, userID string) (*route.Route, error) SaveRouteFunc func(accountID string, route *route.Route) error UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error) @@ -325,9 +325,9 @@ func (am *MockAccountManager) UpdatePeer(accountID string, peer *server.Peer) (* } // CreateRoute mock implementation of CreateRoute from server.AccountManager interface -func (am *MockAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) { +func (am *MockAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) { if am.CreateRouteFunc != nil { - return am.CreateRouteFunc(accountID, network, peer, description, netID, masquerade, metric, enabled) + return am.CreateRouteFunc(accountID, network, peer, description, netID, masquerade, metric, groups, enabled) } return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented") } diff --git a/management/server/peer.go b/management/server/peer.go index 9c3d41f53..e8bd72249 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -121,7 +121,7 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er // fetch all the peers that have access to the user's peers for _, peer := range peers { - aclPeers := am.getPeersByACL(account, peer.Key) + aclPeers := account.getPeersByACL(peer.Key) for _, p := range aclPeers { peersMap[p.Key] = p } @@ -294,8 +294,8 @@ func (am *DefaultAccountManager) GetNetworkMap(peerPubKey string) (*NetworkMap, return nil, err } - aclPeers := am.getPeersByACL(account, peerPubKey) - routesUpdate := account.GetPeersRoutes(append(aclPeers, account.Peers[peerPubKey])) + aclPeers := account.getPeersByACL(peerPubKey) + routesUpdate := account.getRoutesToSync(peerPubKey, aclPeers) var zones []nbdns.CustomZone peersCustomZone := getPeersCustomZone(account, am.dnsDomain) @@ -534,9 +534,9 @@ func (am *DefaultAccountManager) UpdatePeerMeta(peerPubKey string, meta PeerSyst } // getPeersByACL returns all peers that given peer has access to. -func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey string) []*Peer { +func (a *Account) getPeersByACL(peerPubKey string) []*Peer { var peers []*Peer - srcRules, dstRules := account.GetPeerRules(peerPubKey) + srcRules, dstRules := a.GetPeerRules(peerPubKey) groups := map[string]*Group{} for _, r := range srcRules { @@ -545,7 +545,7 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey stri } if r.Flow == TrafficFlowBidirect { for _, gid := range r.Destination { - if group, ok := account.Groups[gid]; ok { + if group, ok := a.Groups[gid]; ok { groups[gid] = group } } @@ -558,7 +558,7 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey stri } if r.Flow == TrafficFlowBidirect { for _, gid := range r.Source { - if group, ok := account.Groups[gid]; ok { + if group, ok := a.Groups[gid]; ok { groups[gid] = group } } @@ -568,13 +568,13 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey stri peersSet := make(map[string]struct{}) for _, g := range groups { for _, pid := range g.Peers { - peer, ok := account.Peers[pid] + peer, ok := a.Peers[pid] if !ok { log.Warnf( "peer %s found in group %s but doesn't belong to account %s", pid, g.ID, - account.Id, + a.Id, ) continue } diff --git a/management/server/route.go b/management/server/route.go index feef82533..36fd39c2d 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -26,6 +26,8 @@ const ( UpdateRouteEnabled // UpdateRouteNetworkIdentifier indicates a route net ID update operation UpdateRouteNetworkIdentifier + // UpdateRouteGroups indicates a group list update operation + UpdateRouteGroups ) // RouteUpdateOperationType operation type @@ -47,6 +49,8 @@ func (t RouteUpdateOperationType) String() string { return "UpdateRouteEnabled" case UpdateRouteNetworkIdentifier: return "UpdateRouteNetworkIdentifier" + case UpdateRouteGroups: + return "UpdateRouteGroups" default: return "InvalidOperation" } @@ -114,7 +118,7 @@ func (am *DefaultAccountManager) checkPrefixPeerExists(accountID, peer string, p } // CreateRoute creates and saves a new route -func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) { +func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -148,6 +152,11 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, de return nil, status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar) } + err = validateGroups(groups, account.Groups) + if err != nil { + return nil, err + } + newRoute.Peer = peer newRoute.ID = xid.New().String() newRoute.Network = newPrefix @@ -157,6 +166,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, de newRoute.Masquerade = masquerade newRoute.Metric = metric newRoute.Enabled = enabled + newRoute.Groups = groups if account.Routes == nil { account.Routes = make(map[string]*route.Route) @@ -210,6 +220,11 @@ func (am *DefaultAccountManager) SaveRoute(accountID string, routeToSave *route. } } + err = validateGroups(routeToSave.Groups, account.Groups) + if err != nil { + return err + } + account.Routes[routeToSave.ID] = routeToSave account.Network.IncSerial() @@ -300,6 +315,12 @@ func (am *DefaultAccountManager) UpdateRoute(accountID, routeID string, operatio return nil, status.Errorf(status.InvalidArgument, "failed to parse enabled %s, not boolean", operation.Values[0]) } newRoute.Enabled = enabled + case UpdateRouteGroups: + err = validateGroups(operation.Values, account.Groups) + if err != nil { + return nil, err + } + newRoute.Groups = operation.Values } } diff --git a/management/server/route_test.go b/management/server/route_test.go index 8acb95b2e..1bd0598cc 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -8,8 +8,13 @@ import ( "testing" ) -const peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=" -const peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI=" +const ( + peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=" + peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI=" + routeGroup1 = "routeGroup1" + routeGroup2 = "routeGroup2" + routeInvalidGroup1 = "routeInvalidGroup1" +) func TestCreateRoute(t *testing.T) { @@ -21,6 +26,7 @@ func TestCreateRoute(t *testing.T) { masquerade bool metric int enabled bool + groups []string } testCases := []struct { @@ -40,6 +46,7 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.NoError, shouldCreate: true, @@ -52,10 +59,11 @@ func TestCreateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, }, { - name: "Bad Prefix", + name: "Bad Prefix Should Fail", inputArgs: input{ network: "192.168.0.0/34", netID: "happy", @@ -64,12 +72,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Bad Peer", + name: "Bad Peer Should Fail", inputArgs: input{ network: "192.168.0.0/16", netID: "happy", @@ -78,12 +87,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Empty Peer", + name: "Empty Peer Should Create", inputArgs: input{ network: "192.168.0.0/16", netID: "happy", @@ -92,6 +102,7 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: false, + groups: []string{routeGroup1}, }, errFunc: require.NoError, shouldCreate: true, @@ -104,10 +115,11 @@ func TestCreateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: false, + Groups: []string{routeGroup1}, }, }, { - name: "Large Metric", + name: "Large Metric Should Fail", inputArgs: input{ network: "192.168.0.0/16", peer: peer1Key, @@ -116,12 +128,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 99999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Small Metric", + name: "Small Metric Should Fail", inputArgs: input{ network: "192.168.0.0/16", netID: "happy", @@ -130,12 +143,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 0, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Large NetID", + name: "Large NetID Should Fail", inputArgs: input{ network: "192.168.0.0/16", peer: peer1Key, @@ -144,12 +158,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Small NetID", + name: "Small NetID Should Fail", inputArgs: input{ network: "192.168.0.0/16", netID: "", @@ -158,6 +173,52 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Empty Group List Should Fail", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "NewId", + peer: peer1Key, + description: "", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{}, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Empty Group ID string Should Fail", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "NewId", + peer: peer1Key, + description: "", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{""}, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Invalid Group Should Fail", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "NewId", + peer: peer1Key, + description: "", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeInvalidGroup1}, }, errFunc: require.Error, shouldCreate: false, @@ -183,6 +244,7 @@ func TestCreateRoute(t *testing.T) { testCase.inputArgs.netID, testCase.inputArgs.masquerade, testCase.inputArgs.metric, + testCase.inputArgs.groups, testCase.inputArgs.enabled, ) @@ -220,6 +282,7 @@ func TestSaveRoute(t *testing.T) { newPeer *string newMetric *int newPrefix *netip.Prefix + newGroups []string skipCopying bool shouldCreate bool errFunc require.ErrorAssertionFunc @@ -237,10 +300,12 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newPeer: &validPeer, newMetric: &validMetric, newPrefix: &validPrefix, + newGroups: []string{routeGroup2}, errFunc: require.NoError, shouldCreate: true, expectedRoute: &route.Route{ @@ -253,10 +318,11 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: validMetric, Enabled: true, + Groups: []string{routeGroup2}, }, }, { - name: "Bad Prefix", + name: "Bad Prefix Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -267,12 +333,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newPrefix: &invalidPrefix, errFunc: require.Error, }, { - name: "Bad Peer", + name: "Bad Peer Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -283,12 +350,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newPeer: &invalidPeer, errFunc: require.Error, }, { - name: "Invalid Metric", + name: "Invalid Metric Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -299,12 +367,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newMetric: &invalidMetric, errFunc: require.Error, }, { - name: "Invalid NetID", + name: "Invalid NetID Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -315,12 +384,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newMetric: &invalidMetric, errFunc: require.Error, }, { - name: "Nil Route", + name: "Nil Route Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -331,10 +401,62 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, skipCopying: true, errFunc: require.Error, }, + { + name: "Empty Group List Should Fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newGroups: []string{}, + errFunc: require.Error, + }, + { + name: "Empty Group ID String Should Fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newGroups: []string{""}, + errFunc: require.Error, + }, + { + name: "Invalid Group Should Fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newGroups: []string{routeInvalidGroup1}, + errFunc: require.Error, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -370,6 +492,10 @@ func TestSaveRoute(t *testing.T) { if testCase.newPrefix != nil { routeToSave.Network = *testCase.newPrefix } + + if testCase.newGroups != nil { + routeToSave.Groups = testCase.newGroups + } } err = am.SaveRoute(account.Id, routeToSave) @@ -409,6 +535,7 @@ func TestUpdateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, } testCases := []struct { @@ -423,7 +550,7 @@ func TestUpdateRoute(t *testing.T) { name: "Happy Path Single OPS", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{peer2Key}, }, @@ -440,40 +567,45 @@ func TestUpdateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, }, { name: "Happy Path Multiple OPS", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteDescription, Values: []string{"great"}, }, - RouteUpdateOperation{ + { Type: UpdateRouteNetwork, Values: []string{"192.168.0.0/24"}, }, - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{peer2Key}, }, - RouteUpdateOperation{ + { Type: UpdateRouteMetric, Values: []string{"3030"}, }, - RouteUpdateOperation{ + { Type: UpdateRouteMasquerade, Values: []string{"true"}, }, - RouteUpdateOperation{ + { Type: UpdateRouteEnabled, Values: []string{"false"}, }, - RouteUpdateOperation{ + { Type: UpdateRouteNetworkIdentifier, Values: []string{"megaRoute"}, }, + { + Type: UpdateRouteGroups, + Values: []string{routeGroup2}, + }, }, errFunc: require.NoError, shouldCreate: true, @@ -487,23 +619,24 @@ func TestUpdateRoute(t *testing.T) { Masquerade: true, Metric: 3030, Enabled: false, + Groups: []string{routeGroup2}, }, }, { - name: "Empty Values", + name: "Empty Values Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, }, }, errFunc: require.Error, }, { - name: "Multiple Values", + name: "Multiple Values Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{peer2Key, peer1Key}, }, @@ -511,10 +644,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Bad Prefix", + name: "Bad Prefix Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteNetwork, Values: []string{"192.168.0.0/34"}, }, @@ -522,10 +655,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Bad Peer", + name: "Bad Peer Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{"non existing Peer"}, }, @@ -536,7 +669,7 @@ func TestUpdateRoute(t *testing.T) { name: "Empty Peer", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{""}, }, @@ -553,13 +686,14 @@ func TestUpdateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, }, { - name: "Large Network ID", + name: "Large Network ID Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteNetworkIdentifier, Values: []string{"12345678901234567890qwertyuiopqwertyuiop1"}, }, @@ -567,10 +701,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Empty Network ID", + name: "Empty Network ID Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteNetworkIdentifier, Values: []string{""}, }, @@ -578,10 +712,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Invalid Metric", + name: "Invalid Metric Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteMetric, Values: []string{"999999"}, }, @@ -589,16 +723,27 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Invalid Boolean", + name: "Invalid Boolean Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteMasquerade, Values: []string{"yes"}, }, }, errFunc: require.Error, }, + { + name: "Invalid Group Should Fail", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + { + Type: UpdateRouteGroups, + Values: []string{routeInvalidGroup1}, + }, + }, + errFunc: require.Error, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -697,6 +842,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, } am, err := createRouterManager(t) @@ -714,7 +860,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes") createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network.String(), baseRoute.Peer, - baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, false) + baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, false) require.NoError(t, err) noDisabledRoutes, err := am.GetNetworkMap(peer1Key) @@ -734,7 +880,14 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { peer2Routes, err := am.GetNetworkMap(peer2Key) require.NoError(t, err) - require.Len(t, peer2Routes.Routes, 1, "we should receive one route for peer2") + require.Len(t, peer2Routes.Routes, 0, "no routes for peers not in the distribution group") + + err = am.GroupAddPeer(account.Id, routeGroup1, peer2Key) + require.NoError(t, err) + + peer2Routes, err = am.GetNetworkMap(peer2Key) + require.NoError(t, err) + require.Len(t, peer2Routes.Routes, 1, "we should receive one route") require.True(t, peer1Routes.Routes[0].IsEqual(peer2Routes.Routes[0]), "routes should be the same for peers in the same group") newGroup := &Group{ @@ -844,6 +997,26 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er if err != nil { return nil, err } + newGroup := &Group{ + ID: routeGroup1, + Name: routeGroup1, + Peers: []string{peer1Key}, + } + err = am.SaveGroup(accountID, newGroup) + if err != nil { + return nil, err + } + + newGroup = &Group{ + ID: routeGroup2, + Name: routeGroup2, + Peers: []string{peer1Key}, + } + + err = am.SaveGroup(accountID, newGroup) + if err != nil { + return nil, err + } return am.Store.GetAccount(account.Id) } diff --git a/route/route.go b/route/route.go index 4173783df..62a76db82 100644 --- a/route/route.go +++ b/route/route.go @@ -73,6 +73,7 @@ type Route struct { Masquerade bool Metric int Enabled bool + Groups []string } // Copy copies a route object @@ -87,6 +88,7 @@ func (r *Route) Copy() *Route { Metric: r.Metric, Masquerade: r.Masquerade, Enabled: r.Enabled, + Groups: r.Groups, } } @@ -100,7 +102,8 @@ func (r *Route) IsEqual(other *Route) bool { other.Peer == r.Peer && other.Metric == r.Metric && other.Masquerade == r.Masquerade && - other.Enabled == r.Enabled + other.Enabled == r.Enabled && + compareGroupsList(r.Groups, other.Groups) } // ParseNetwork Parses a network prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6 @@ -122,3 +125,23 @@ func ParseNetwork(networkString string) (NetworkType, netip.Prefix, error) { return IPv4Network, masked, nil } + +func compareGroupsList(list, other []string) bool { + if len(list) != len(other) { + return false + } + for _, id := range list { + match := false + for _, otherID := range other { + if id == otherID { + match = true + break + } + } + if !match { + return false + } + } + + return true +} From cfed36ff8dc35e0532043c4216773b146199a268 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 6 Dec 2022 15:37:30 +0100 Subject: [PATCH 15/30] Handle peer deletion and state update (#611) If peer is deleted in the console, we set its state as needs login On Down command we clean any previous state errors this prevents need for daemon restart Removed state error wrapping when engine exits, log is enough --- client/internal/connect.go | 3 +++ client/server/server.go | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/internal/connect.go b/client/internal/connect.go index 2c55c2b28..c13df0888 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -177,6 +177,9 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *nbStatus.Sta err = backoff.Retry(operation, backOff) if err != nil { log.Debugf("exiting client retry loop due to unrecoverable error: %s", err) + if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) { + state.Set(StatusNeedsLogin) + } return err } return nil diff --git a/client/server/server.go b/client/server/server.go index 2beeca609..a5fb3423b 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -335,7 +335,7 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin } // Up starts engine work in the daemon. -func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpResponse, error) { +func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpResponse, error) { s.mutex.Lock() defer s.mutex.Unlock() @@ -375,7 +375,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR go func() { if err := internal.RunClient(ctx, s.config, s.statusRecorder); err != nil { - log.Errorf("run client connection: %v", state.Wrap(err)) + log.Errorf("run client connection: %v", err) return } }() @@ -384,7 +384,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR } // Down engine work in the daemon. -func (s *Server) Down(ctx context.Context, msg *proto.DownRequest) (*proto.DownResponse, error) { +func (s *Server) Down(_ context.Context, _ *proto.DownRequest) (*proto.DownResponse, error) { s.mutex.Lock() defer s.mutex.Unlock() @@ -392,6 +392,8 @@ func (s *Server) Down(ctx context.Context, msg *proto.DownRequest) (*proto.DownR return nil, fmt.Errorf("service is not up") } s.actCancel() + state := internal.CtxGetState(s.rootCtx) + state.Set(internal.StatusIdle) return &proto.DownResponse{}, nil } @@ -425,7 +427,7 @@ func (s *Server) Status( } // GetConfig of the daemon. -func (s *Server) GetConfig(ctx context.Context, msg *proto.GetConfigRequest) (*proto.GetConfigResponse, error) { +func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto.GetConfigResponse, error) { s.mutex.Lock() defer s.mutex.Unlock() From 1835dd3e3c4b4e2c1e4c53f2d7f73aa07ad03368 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 7 Dec 2022 18:53:48 +0100 Subject: [PATCH 16/30] Remove wiretrustee conflict checks (#615) --- .goreleaser.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c6b0a4c4a..074a5dc55 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -74,11 +74,6 @@ nfpms: formats: - deb - replaces: - - wiretrustee - conflicts: - - wiretrustee - scripts: postinstall: "release_files/post_install.sh" preremove: "release_files/pre_remove.sh" @@ -93,12 +88,6 @@ nfpms: formats: - rpm - replaces: - - wiretrustee - - conflicts: - - wiretrustee - scripts: postinstall: "release_files/post_install.sh" preremove: "release_files/pre_remove.sh" @@ -348,8 +337,6 @@ brews: license: "BSD3" test: | system "#{bin}/{{ .ProjectName }} version" - conflicts: - - wiretrustee uploads: - name: debian From 94803417cfdb399944a177ab1516d1489cc0c886 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 7 Dec 2022 22:06:43 +0100 Subject: [PATCH 17/30] Generate validation certificate from mandatory JWK fields (#614) When there is no X5c we will use N and E fields of a JWK to generate the public RSA and a Pem certificate --- management/server/http/middleware/handler.go | 97 +++++++++++++++++--- 1 file changed, 84 insertions(+), 13 deletions(-) diff --git a/management/server/http/middleware/handler.go b/management/server/http/middleware/handler.go index 89b8410dd..8725a3d27 100644 --- a/management/server/http/middleware/handler.go +++ b/management/server/http/middleware/handler.go @@ -1,19 +1,28 @@ package middleware import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/binary" "encoding/json" + "encoding/pem" "errors" + "fmt" "github.com/golang-jwt/jwt" + log "github.com/sirupsen/logrus" + "math/big" "net/http" ) -// Jwks is a collection of JSONWebKeys obtained from Config.HttpServerConfig.AuthKeysLocation +// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation type Jwks struct { - Keys []JSONWebKeys `json:"keys"` + Keys []JSONWebKey `json:"keys"` } -// JSONWebKeys is a representation of a Jason Web Key -type JSONWebKeys struct { +// JSONWebKey is a representation of a Jason Web Key +type JSONWebKey struct { Kty string `json:"kty"` Kid string `json:"kid"` Use string `json:"use"` @@ -45,7 +54,7 @@ func NewJwtMiddleware(issuer string, audience string, keysLocation string) (*JWT cert, err := getPemCert(token, keys) if err != nil { - panic(err.Error()) + return nil, err } result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) @@ -74,18 +83,80 @@ func getPemKeys(keysLocation string) (*Jwks, error) { } func getPemCert(token *jwt.Token, jwks *Jwks) (string, error) { + // todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time cert := "" for k := range jwks.Keys { - if token.Header["kid"] == jwks.Keys[k].Kid { - cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----" + if token.Header["kid"] != jwks.Keys[k].Kid { + continue } + + if len(jwks.Keys[k].X5c) != 0 { + cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----" + return cert, nil + } + log.Debugf("generating validation pem from JWK") + return generatePemFromJWK(jwks.Keys[k]) } - if cert == "" { - err := errors.New("unable to find appropriate key") - return cert, err - } - - return cert, nil + return "", errors.New("unable to find appropriate key") +} + +func generatePemFromJWK(jwk JSONWebKey) (string, error) { + decodedModulus, err := base64.RawURLEncoding.DecodeString(jwk.N) + if err != nil { + return "", fmt.Errorf("unable to decode JWK modulus, error: %s", err) + } + + intModules := big.NewInt(0) + intModules.SetBytes(decodedModulus) + + exponent, err := convertExponentStringToInt(jwk.E) + if err != nil { + return "", fmt.Errorf("unable to decode JWK exponent, error: %s", err) + } + + publicKey := &rsa.PublicKey{ + N: intModules, + E: exponent, + } + + derKey, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", fmt.Errorf("unable to convert public key to DER, error: %s", err) + } + + block := &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: derKey, + } + + var out bytes.Buffer + err = pem.Encode(&out, block) + if err != nil { + return "", fmt.Errorf("unable to encode Pem block , error: %s", err) + } + + return out.String(), nil +} + +func convertExponentStringToInt(stringExponent string) (int, error) { + decodedString, err := base64.StdEncoding.DecodeString(stringExponent) + if err != nil { + return 0, err + } + exponentBytes := decodedString + if len(decodedString) < 8 { + exponentBytes = make([]byte, 8-len(decodedString), 8) + exponentBytes = append(exponentBytes, decodedString...) + } + + bytesReader := bytes.NewReader(exponentBytes) + var exponent uint64 + err = binary.Read(bytesReader, binary.BigEndian, &exponent) + if err != nil { + return 0, err + } + + return int(exponent), nil } From 5d544ae55e0342927ea8ccd83e6163d2a5c58e14 Mon Sep 17 00:00:00 2001 From: Krzysztof Nazarewski <3494992+nazarewk@users.noreply.github.com> Date: Thu, 8 Dec 2022 13:19:55 +0100 Subject: [PATCH 18/30] HA Network Routes: prevent routing directly-accessible networks through VPN interface (#612) Prevent routing peer to add routes from the same HA group as client routes --- client/internal/routemanager/client.go | 5 +- client/internal/routemanager/manager.go | 20 ++++--- client/internal/routemanager/manager_test.go | 56 ++++++++++++++++++-- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 263c7c3e6..4c022ef46 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -3,12 +3,13 @@ package routemanager import ( "context" "fmt" + "net/netip" + "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" log "github.com/sirupsen/logrus" - "net/netip" ) type routerPeerStatus struct { @@ -52,7 +53,7 @@ func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, st return client } -func getClientNetworkID(input *route.Route) string { +func getHANetworkID(input *route.Route) string { return input.NetID + "-" + input.Network.String() } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 4527ae0cb..70c3570b3 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -3,13 +3,14 @@ package routemanager import ( "context" "fmt" + "runtime" + "sync" + "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" log "github.com/sirupsen/logrus" - "runtime" - "sync" ) // Manager is a route manager interface @@ -147,16 +148,24 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro newClientRoutesIDMap := make(map[string][]*route.Route) newServerRoutesMap := make(map[string]*route.Route) + ownNetworkIDs := make(map[string]bool) for _, newRoute := range newRoutes { - // only linux is supported for now + networkID := getHANetworkID(newRoute) if newRoute.Peer == m.pubKey { + ownNetworkIDs[networkID] = true + // only linux is supported for now if runtime.GOOS != "linux" { log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS) continue } newServerRoutesMap[newRoute.ID] = newRoute - } else { + } + } + + for _, newRoute := range newRoutes { + networkID := getHANetworkID(newRoute) + if !ownNetworkIDs[networkID] { // if prefix is too small, lets assume is a possible default route which is not yet supported // we skip this route management if newRoute.Network.Bits() < 7 { @@ -164,8 +173,7 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro system.NetbirdVersion(), newRoute.Network) continue } - clientNetworkID := getClientNetworkID(newRoute) - newClientRoutesIDMap[clientNetworkID] = append(newClientRoutesIDMap[clientNetworkID], newRoute) + newClientRoutesIDMap[networkID] = append(newClientRoutesIDMap[networkID], newRoute) } } diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index f88aeb53d..58cce2ad0 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -3,13 +3,14 @@ package routemanager import ( "context" "fmt" + "net/netip" + "runtime" + "testing" + "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" "github.com/stretchr/testify/require" - "net/netip" - "runtime" - "testing" ) // send 5 routes, one for server and 4 for clients, one normal and 2 HA and one small @@ -336,6 +337,55 @@ func TestManagerUpdateRoutes(t *testing.T) { serverRoutesExpected: 0, clientNetworkWatchersExpected: 0, }, + { + name: "HA server should not register routes from the same HA group", + inputRoutes: []*route.Route{ + { + ID: "l1", + NetID: "routeA", + Peer: localPeerKey, + Network: netip.MustParsePrefix("100.64.251.250/30"), + NetworkType: route.IPv4Network, + Metric: 9999, + Masquerade: false, + Enabled: true, + }, + { + ID: "l2", + NetID: "routeA", + Peer: localPeerKey, + Network: netip.MustParsePrefix("8.8.9.8/32"), + NetworkType: route.IPv4Network, + Metric: 9999, + Masquerade: false, + Enabled: true, + }, + { + ID: "r1", + NetID: "routeA", + Peer: remotePeerKey1, + Network: netip.MustParsePrefix("100.64.251.250/30"), + NetworkType: route.IPv4Network, + Metric: 9999, + Masquerade: false, + Enabled: true, + }, + { + ID: "r2", + NetID: "routeC", + Peer: remotePeerKey1, + Network: netip.MustParsePrefix("8.8.9.9/32"), + NetworkType: route.IPv4Network, + Metric: 9999, + Masquerade: false, + Enabled: true, + }, + }, + inputSerial: 1, + shouldCheckServerRoutes: runtime.GOOS == "linux", + serverRoutesExpected: 2, + clientNetworkWatchersExpected: 1, + }, } for n, testCase := range testCases { From 2dec9989c6334f272732c2f9b1c2f2c53301adde Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 8 Dec 2022 15:15:17 +0100 Subject: [PATCH 19/30] Use latest tag for dashboard (#617) Using the latest tag will align with the dashboard's new release cycle that relies on tags --- infrastructure_files/docker-compose.yml.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure_files/docker-compose.yml.tmpl b/infrastructure_files/docker-compose.yml.tmpl index 948732c3c..fd14717db 100644 --- a/infrastructure_files/docker-compose.yml.tmpl +++ b/infrastructure_files/docker-compose.yml.tmpl @@ -2,7 +2,7 @@ version: "3" services: #UI dashboard dashboard: - image: wiretrustee/dashboard:main + image: wiretrustee/dashboard:latest restart: unless-stopped ports: - 80:80 From 739afea4b40386cdb74c0749088a876d67b4fd9c Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 8 Dec 2022 15:15:50 +0100 Subject: [PATCH 20/30] Filter routes to sync from same HA group (#618) An additional check and filter for routes that are part of the same HA group where the peer is a routing peer --- client/internal/routemanager/client.go | 4 --- client/internal/routemanager/manager.go | 4 +-- management/server/account.go | 42 +++++++++++++++++++------ management/server/account_test.go | 18 ++++++++++- route/route.go | 5 +++ 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 4c022ef46..e35368060 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -53,10 +53,6 @@ func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, st return client } -func getHANetworkID(input *route.Route) string { - return input.NetID + "-" + input.Network.String() -} - func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus { routePeerStatuses := make(map[string]routerPeerStatus) for _, r := range c.routes { diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 70c3570b3..e05723016 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -151,7 +151,7 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro ownNetworkIDs := make(map[string]bool) for _, newRoute := range newRoutes { - networkID := getHANetworkID(newRoute) + networkID := route.GetHAUniqueID(newRoute) if newRoute.Peer == m.pubKey { ownNetworkIDs[networkID] = true // only linux is supported for now @@ -164,7 +164,7 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro } for _, newRoute := range newRoutes { - networkID := getHANetworkID(newRoute) + networkID := route.GetHAUniqueID(newRoute) if !ownNetworkIDs[networkID] { // if prefix is too small, lets assume is a possible default route which is not yet supported // we skip this route management diff --git a/management/server/account.go b/management/server/account.go index 3eff78ad5..f4a00b0b5 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -142,17 +142,35 @@ type UserInfo struct { // getRoutesToSync returns the enabled routes for the peer ID and the routes // from the ACL peers that have distribution groups associated with the peer ID func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Route { - routes := a.getEnabledRoutesByPeer(peerID) + routes, peerDisabledRoutes := a.getEnabledAndDisabledRoutesByPeer(peerID) + peerRoutesMembership := make(lookupMap) + for _, r := range append(routes, peerDisabledRoutes...) { + peerRoutesMembership[route.GetHAUniqueID(r)] = struct{}{} + } + groupListMap := a.getPeerGroups(peerID) for _, peer := range aclPeers { - activeRoutes := a.getEnabledRoutesByPeer(peer.Key) - filteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap) + activeRoutes, _ := a.getEnabledAndDisabledRoutesByPeer(peer.Key) + groupFilteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap) + filteredRoutes := a.filterRoutesFromPeersOfSameHAGroup(groupFilteredRoutes, peerRoutesMembership) routes = append(routes, filteredRoutes...) } return routes } +// filterRoutesByHAMembership filters and returns a list of routes that don't share the same HA route membership +func (a *Account) filterRoutesFromPeersOfSameHAGroup(routes []*route.Route, peerMemberships lookupMap) []*route.Route { + var filteredRoutes []*route.Route + for _, r := range routes { + _, found := peerMemberships[route.GetHAUniqueID(r)] + if !found { + filteredRoutes = append(filteredRoutes, r) + } + } + return filteredRoutes +} + // filterRoutesByGroups returns a list with routes that have distribution groups in the group's map func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap lookupMap) []*route.Route { var filteredRoutes []*route.Route @@ -168,17 +186,21 @@ func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap looku return filteredRoutes } -// getEnabledRoutesByPeer returns a list of routes of a given peer -func (a *Account) getEnabledRoutesByPeer(peerPubKey string) []*route.Route { +// getEnabledAndDisabledRoutesByPeer returns the enabled and disabled lists of routes that belong to a peer +func (a *Account) getEnabledAndDisabledRoutesByPeer(peerPubKey string) ([]*route.Route, []*route.Route) { //TODO Peer.ID migration: we will need to replace search by Peer.ID here - var routes []*route.Route + var enabledRoutes []*route.Route + var disabledRoutes []*route.Route for _, r := range a.Routes { - if r.Peer == peerPubKey && r.Enabled { - routes = append(routes, r) - continue + if r.Peer == peerPubKey { + if r.Enabled { + enabledRoutes = append(enabledRoutes, r) + continue + } + disabledRoutes = append(disabledRoutes, r) } } - return routes + return enabledRoutes, disabledRoutes } // GetRoutesByPrefix return list of routes by account and route prefix diff --git a/management/server/account_test.go b/management/server/account_test.go index 667d582be..31480eb5d 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1080,6 +1080,10 @@ func TestAccount_GetRoutesToSync(t *testing.T) { if err != nil { t.Fatal(err) } + _, prefix2, err := route.ParseNetwork("192.168.0.0/24") + if err != nil { + t.Fatal(err) + } account := &Account{ Peers: map[string]*Peer{ "peer-1": {Key: "peer-1"}, "peer-2": {Key: "peer-2"}, "peer-3": {Key: "peer-1"}, @@ -1100,6 +1104,18 @@ func TestAccount_GetRoutesToSync(t *testing.T) { }, "route-2": { ID: "route-2", + Network: prefix2, + NetID: "network-2", + Description: "network-2", + Peer: "peer-2", + NetworkType: 0, + Masquerade: false, + Metric: 999, + Enabled: true, + Groups: []string{"group1"}, + }, + "route-3": { + ID: "route-3", Network: prefix, NetID: "network-1", Description: "network-1", @@ -1120,8 +1136,8 @@ func TestAccount_GetRoutesToSync(t *testing.T) { for _, r := range routes { routeIDs[r.ID] = struct{}{} } - assert.Contains(t, routeIDs, "route-1") assert.Contains(t, routeIDs, "route-2") + assert.Contains(t, routeIDs, "route-3") emptyRoutes := account.getRoutesToSync("peer-3", []*Peer{{Key: "peer-1"}, {Key: "peer-2"}}) diff --git a/route/route.go b/route/route.go index 62a76db82..eea996281 100644 --- a/route/route.go +++ b/route/route.go @@ -145,3 +145,8 @@ func compareGroupsList(list, other []string) bool { return true } + +// GetHAUniqueID returns a highly available route ID by combining Network ID and Network range address +func GetHAUniqueID(input *Route) string { + return input.NetID + "-" + input.Network.String() +} From cf27bd8951c6cf4b0e6f2d6ec65f3d1bd7536b19 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 8 Dec 2022 19:45:33 +0100 Subject: [PATCH 21/30] Export single account domain variable --- infrastructure_files/base.setup.env | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index a4dcc92d6..ccab2ae4d 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -60,3 +60,4 @@ export MGMT_VOLUMESUFFIX export SIGNAL_VOLUMESUFFIX export LETSENCRYPT_VOLUMESUFFIX export NETBIRD_DISABLE_ANONYMOUS_METRICS +export NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN \ No newline at end of file From f684baa6657105930a723db0c5477f7d331b7d4c Mon Sep 17 00:00:00 2001 From: braginini Date: Sun, 11 Dec 2022 20:01:17 +0100 Subject: [PATCH 22/30] Add Events HTTP API --- management/server/http/api/openapi.yml | 60 ++++++++++++++++++++++++- management/server/http/api/types.gen.go | 33 ++++++++++++++ management/server/http/events.go | 55 +++++++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 management/server/http/events.go diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index c25e14fa0..e70f9ebc7 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: @@ -501,7 +503,40 @@ components: 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 + operation: + description: The operation (or action) that occurred during the event + type: string + operation_code: + description: The numeric code of the operation (or action) that occurred during the event + type: integer + type: + description: The type of the event that occurred. Indicates whether it was a management or device event + type: string + enum: ["device", "management"] + 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 + required: + - id + - timestamp + - operation + - operation_code + - type + - initiator_id + - target_id responses: not_found: description: Resource not found @@ -1573,5 +1608,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 3ba8e6775..a4b82d3b6 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -11,6 +11,12 @@ const ( BearerAuthScopes = "BearerAuth.Scopes" ) +// Defines values for EventType. +const ( + EventTypeDevice EventType = "device" + EventTypeManagement EventType = "management" +) + // Defines values for GroupPatchOperationOp. const ( GroupPatchOperationOpAdd GroupPatchOperationOp = "add" @@ -97,6 +103,33 @@ const ( UserStatusInvited UserStatus = "invited" ) +// Event defines model for Event. +type Event struct { + // 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"` + + // Operation The operation (or action) that occurred during the event + Operation string `json:"operation"` + + // OperationCode The numeric code of the operation (or action) that occurred during the event + OperationCode int `json:"operation_code"` + + // 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"` + + // Type The type of the event that occurred. Indicates whether it was a management or device event + Type EventType `json:"type"` +} + +// EventType The type of the event that occurred. Indicates whether it was a management or device event +type EventType 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 000000000..3331f14b2 --- /dev/null +++ b/management/server/http/events.go @@ -0,0 +1,55 @@ +package http + +import ( + "fmt" + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/event" + "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) + _, _, err := h.accountManager.GetAccountFromToken(claims) + if err != nil { + log.Error(err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + var groups []*api.Event + + util.WriteJSONObject(w, groups) +} + +func toEventResponse(event *event.Event) *api.Event { + return &api.Event{ + Id: fmt.Sprint(event.ID), + InitiatorId: event.ModifierID, + Operation: event.Operation, + OperationCode: int(event.OperationCode), + TargetId: event.TargetID, + Timestamp: event.Timestamp, + Type: event.Type, + } +} From 4440ad12711399298f9f7197522286d7549bad84 Mon Sep 17 00:00:00 2001 From: braginini Date: Sun, 11 Dec 2022 21:16:47 +0100 Subject: [PATCH 23/30] Fetch Activity events --- management/cmd/management.go | 4 +-- management/server/account.go | 13 ++++---- .../server/{event => activity}/event.go | 6 ++-- .../server/{event => activity}/sqlite.go | 31 ++++++++++--------- .../server/{event => activity}/sqlite_test.go | 4 +-- management/server/event.go | 8 +++++ management/server/http/events.go | 21 +++++++++---- management/server/http/handler.go | 3 ++ management/server/peer.go | 10 +++--- 9 files changed, 61 insertions(+), 39 deletions(-) rename management/server/{event => activity}/event.go (94%) rename management/server/{event => activity}/sqlite.go (80%) rename management/server/{event => activity}/sqlite_test.go (95%) create mode 100644 management/server/event.go diff --git a/management/cmd/management.go b/management/cmd/management.go index 93b487c63..6d6769fc6 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -9,7 +9,7 @@ import ( "fmt" "github.com/google/uuid" "github.com/miekg/dns" - "github.com/netbirdio/netbird/management/server/event" + "github.com/netbirdio/netbird/management/server/activity" httpapi "github.com/netbirdio/netbird/management/server/http" "github.com/netbirdio/netbird/management/server/metrics" "github.com/netbirdio/netbird/management/server/telemetry" @@ -143,7 +143,7 @@ var ( if disableSingleAccMode { mgmtSingleAccModeDomain = "" } - eventStore, err := event.NewSQLiteStore(config.Datadir) + eventStore, err := activity.NewSQLiteStore(config.Datadir) if err != nil { return err } diff --git a/management/server/account.go b/management/server/account.go index f4a00b0b5..cdb46fccb 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -6,7 +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/event" + "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" @@ -88,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 { @@ -100,7 +101,7 @@ type DefaultAccountManager struct { idpManager idp.Manager cacheManager cache.CacheInterface[[]*idp.UserData] ctx context.Context - eventStore event.Store + 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. @@ -437,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, eventStore event.Store) (*DefaultAccountManager, error) { + singleAccountModeDomain string, dnsDomain string, eventStore activity.Store) (*DefaultAccountManager, error) { am := &DefaultAccountManager{ Store: store, peersUpdateManager: peersUpdateManager, @@ -797,10 +798,10 @@ func (am *DefaultAccountManager) handleNewUserAccount(domainAcc *Account, claims return nil, err } - opEvent := event.Event{ + opEvent := &activity.Event{ Timestamp: time.Now(), - Type: event.ManagementEvent, - OperationCode: event.UserJoinedOperation, + Type: activity.ManagementEvent, + OperationCode: activity.UserJoinedOperation, AccountID: account.Id, TargetID: claims.UserId, ModifierID: claims.UserId, diff --git a/management/server/event/event.go b/management/server/activity/event.go similarity index 94% rename from management/server/event/event.go rename to management/server/activity/event.go index e3a650f05..d39083c6d 100644 --- a/management/server/event/event.go +++ b/management/server/activity/event.go @@ -1,4 +1,4 @@ -package event +package activity import "time" @@ -44,9 +44,9 @@ type Operation int // Store provides an interface to store or stream events. type Store interface { // Save an event in the store - Save(event Event) (*Event, error) + 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) + Get(accountID string, offset, limit int, descending bool) ([]*Event, error) // Close the sink flushing events if necessary Close() error } diff --git a/management/server/event/sqlite.go b/management/server/activity/sqlite.go similarity index 80% rename from management/server/event/sqlite.go rename to management/server/activity/sqlite.go index 266f60fa7..626821fac 100644 --- a/management/server/event/sqlite.go +++ b/management/server/activity/sqlite.go @@ -1,4 +1,4 @@ -package event +package activity import ( "database/sql" @@ -19,7 +19,7 @@ const ( " target TEXT);" ) -// SQLiteStore is the implementation of the event.Store interface backed by SQLite +// SQLiteStore is the implementation of the activity.Store interface backed by SQLite type SQLiteStore struct { db *sql.DB } @@ -40,11 +40,11 @@ func NewSQLiteStore(dataDir string) (*SQLiteStore, error) { return &SQLiteStore{db: db}, nil } -func processResult(result *sql.Rows) ([]Event, error) { - events := make([]Event, 0) +func processResult(result *sql.Rows) ([]*Event, error) { + events := make([]*Event, 0) for result.Next() { var id int64 - var operation string + var operation Operation var timestamp time.Time var modifier string var target string @@ -55,14 +55,15 @@ func processResult(result *sql.Rows) ([]Event, error) { return nil, err } - events = append(events, Event{ - Timestamp: timestamp, - Operation: operation, - ID: uint64(id), - Type: typ, - ModifierID: modifier, - TargetID: target, - AccountID: account, + events = append(events, &Event{ + Timestamp: timestamp, + OperationCode: operation, + Operation: MessageForOperation(operation), + ID: uint64(id), + Type: typ, + ModifierID: modifier, + TargetID: target, + AccountID: account, }) } @@ -70,7 +71,7 @@ func processResult(result *sql.Rows) ([]Event, error) { } // Get returns "limit" number of events from index ordered descending or ascending by a timestamp -func (store *SQLiteStore) Get(accountID string, offset, limit int, descending bool) ([]Event, error) { +func (store *SQLiteStore) Get(accountID string, offset, limit int, descending bool) ([]*Event, error) { order := "DESC" if !descending { order = "ASC" @@ -91,7 +92,7 @@ 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) { +func (store *SQLiteStore) Save(event *Event) (*Event, error) { stmt, err := store.db.Prepare("INSERT INTO events(operation, timestamp, modifier, target, account, type) VALUES(?, ?, ?, ?, ?, ?)") if err != nil { diff --git a/management/server/event/sqlite_test.go b/management/server/activity/sqlite_test.go similarity index 95% rename from management/server/event/sqlite_test.go rename to management/server/activity/sqlite_test.go index ae91022b5..df27f782a 100644 --- a/management/server/event/sqlite_test.go +++ b/management/server/activity/sqlite_test.go @@ -1,4 +1,4 @@ -package event +package activity import ( "fmt" @@ -18,7 +18,7 @@ func TestNewSQLiteStore(t *testing.T) { accountID := "account_1" for i := 0; i < 10; i++ { - _, err = store.Save(Event{ + _, err = store.Save(&Event{ Timestamp: time.Now(), OperationCode: AddPeerByUserOperation, Type: ManagementEvent, diff --git a/management/server/event.go b/management/server/event.go new file mode 100644 index 000000000..7f7b59583 --- /dev/null +++ b/management/server/event.go @@ -0,0 +1,8 @@ +package server + +import "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) { + return am.eventStore.Get(accountID, 0, 1000, true) +} diff --git a/management/server/http/events.go b/management/server/http/events.go index 3331f14b2..e3f88c983 100644 --- a/management/server/http/events.go +++ b/management/server/http/events.go @@ -3,7 +3,7 @@ package http import ( "fmt" "github.com/netbirdio/netbird/management/server" - "github.com/netbirdio/netbird/management/server/event" + "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" @@ -30,19 +30,28 @@ func NewEvents(accountManager server.AccountManager, authAudience string) *Event // GetEvents list of the given account func (h *Events) GetEvents(w http.ResponseWriter, r *http.Request) { claims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience) - _, _, err := h.accountManager.GetAccountFromToken(claims) + account, user, err := h.accountManager.GetAccountFromToken(claims) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - var groups []*api.Event + accountEvents, err := h.accountManager.GetEvents(account.Id, user.Id) + if err != nil { + util.WriteError(err, w) + return + } + var events []*api.Event + for _, e := range accountEvents { + events = append(events, toEventResponse(e)) + } - util.WriteJSONObject(w, groups) + util.WriteJSONObject(w, events) } -func toEventResponse(event *event.Event) *api.Event { +func toEventResponse(event *activity.Event) *api.Event { + return &api.Event{ Id: fmt.Sprint(event.ID), InitiatorId: event.ModifierID, @@ -50,6 +59,6 @@ func toEventResponse(event *event.Event) *api.Event { OperationCode: int(event.OperationCode), TargetId: event.TargetID, Timestamp: event.Timestamp, - Type: event.Type, + Type: api.EventType(event.Type), } } diff --git a/management/server/http/handler.go b/management/server/http/handler.go index f0d0c5b4a..d4beaec90 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/peer.go b/management/server/peer.go index e8bd72249..a3ade59a6 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -2,7 +2,7 @@ package server import ( nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/server/event" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" "net" "strings" @@ -361,9 +361,9 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* return nil, err } - opEvent := event.Event{ + opEvent := &activity.Event{ Timestamp: time.Now(), - Type: event.ManagementEvent, + Type: activity.ManagementEvent, AccountID: account.Id, } @@ -380,10 +380,10 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* account.SetupKeys[sk.Key] = sk.IncrementUsage() opEvent.ModifierID = sk.Id - opEvent.OperationCode = event.AddPeerWithKeyOperation + opEvent.OperationCode = activity.AddPeerWithKeyOperation } else { opEvent.ModifierID = userID - opEvent.OperationCode = event.AddPeerByUserOperation + opEvent.OperationCode = activity.AddPeerByUserOperation } takenIps := account.getTakenIPs() From c50d07b83f114404b97fdd1ee6dc1b763370d59f Mon Sep 17 00:00:00 2001 From: braginini Date: Mon, 12 Dec 2022 09:10:55 +0100 Subject: [PATCH 24/30] Finalize events API --- management/server/account.go | 28 ++-- management/server/activity/event.go | 109 ++++++++------ management/server/activity/sqlite.go | 39 +++-- management/server/activity/sqlite_test.go | 11 +- management/server/http/api/openapi.yml | 172 +++++++++++----------- management/server/http/api/types.gen.go | 28 ++-- management/server/http/events.go | 15 +- management/server/peer.go | 9 +- management/server/user.go | 3 +- 9 files changed, 217 insertions(+), 197 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index cdb46fccb..3049dcafe 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -511,7 +511,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 } @@ -798,16 +809,15 @@ func (am *DefaultAccountManager) handleNewUserAccount(domainAcc *Account, claims return nil, err } - opEvent := &activity.Event{ - Timestamp: time.Now(), - Type: activity.ManagementEvent, - OperationCode: activity.UserJoinedOperation, - AccountID: account.Id, - TargetID: claims.UserId, - ModifierID: claims.UserId, + event := &activity.Event{ + Timestamp: time.Now(), + Activity: activity.UserJoined, + AccountID: account.Id, + TargetID: claims.UserId, + InitiatorID: claims.UserId, } - _, err = am.eventStore.Save(opEvent) + _, err = am.eventStore.Save(event) if err != nil { return nil, err } diff --git a/management/server/activity/event.go b/management/server/activity/event.go index d39083c6d..85fc4d3a5 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -3,43 +3,69 @@ package activity import "time" const ( - // DeviceEvent describes an event that happened of a device (e.g, connected/disconnected) - DeviceEvent Type = "device" - // ManagementEvent describes an event that happened on a Management service (e.g., user added) - ManagementEvent Type = "management" + // 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 ) const ( - AddPeerByUserOperation Operation = iota - AddPeerWithKeyOperation - UserJoinedOperation + // PeerAddedByUserMessage is a human-readable text message of the PeerAddedByUser activity + PeerAddedByUserMessage string = "New peer added by a user" + // PeerAddedWithSetupKeyMessage is a human-readable text message of the PeerAddedWithSetupKey activity + PeerAddedWithSetupKeyMessage = "New peer added with a setup key" + //UserJoinedMessage is a human-readable text message of the UserJoined activity + UserJoinedMessage string = "New user joined" + //UserInvitedMessage is a human-readable text message of the UserInvited activity + UserInvitedMessage string = "New user invited" + //AccountCreatedMessage is a human-readable text message of the AccountCreated activity + AccountCreatedMessage string = "Account created" ) -const ( - AddPeerByUserOperationMessage string = "Add new peer" - AddPeerWithKeyOperationMessage string = AddPeerByUserOperationMessage - UserJoinedOperationMessage string = "New user joined" -) +// Activity that triggered an Event +type Activity int -// MessageForOperation returns a string message for an Operation -func MessageForOperation(op Operation) string { - switch op { - case AddPeerByUserOperation: - return AddPeerByUserOperationMessage - case AddPeerWithKeyOperation: - return AddPeerWithKeyOperationMessage - case UserJoinedOperation: - return UserJoinedOperationMessage +// Message returns a string representation of an activity +func (a Activity) Message() string { + switch a { + case PeerAddedByUser: + return PeerAddedByUserMessage + case PeerAddedWithSetupKey: + return PeerAddedWithSetupKeyMessage + case UserJoined: + return UserJoinedMessage + case UserInvited: + return UserInvitedMessage + case AccountCreated: + return AccountCreatedMessage default: - return "UNKNOWN_OPERATION" + return "UNKNOWN_ACTIVITY" } } -// Type of the Event -type Type string - -// Operation is an action that triggered an Event -type Operation int +// StringCode returns a string code of the activity +func (a Activity) StringCode() string { + switch a { + case PeerAddedByUser: + return "user.peer.add" + case PeerAddedWithSetupKey: + return "setupkey.peer.add" + case UserJoined: + return "user.join" + case UserInvited: + return "user.invite" + case AccountCreated: + return "account.create" + default: + return "UNKNOWN_ACTIVITY" + } +} // Store provides an interface to store or stream events. type Store interface { @@ -55,31 +81,26 @@ type Store interface { type Event struct { // Timestamp of the event Timestamp time.Time - // Operation that was performed during the event - Operation string - // OperationCode that was performed during the event - OperationCode Operation + // 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 - // Type of the event - Type Type - // ModifierID is the ID of an object that modifies a Target - ModifierID string - // TargetID is the ID of an object that a Modifier modifies + // 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 where event happened + // AccountID is the ID of an account where the event happened AccountID string } // Copy the event func (e *Event) Copy() *Event { return &Event{ - Timestamp: e.Timestamp, - Operation: e.Operation, - ID: e.ID, - Type: e.Type, - ModifierID: e.ModifierID, - TargetID: e.TargetID, - AccountID: e.AccountID, + Timestamp: e.Timestamp, + Activity: e.Activity, + ID: e.ID, + InitiatorID: e.InitiatorID, + TargetID: e.TargetID, + AccountID: e.AccountID, } } diff --git a/management/server/activity/sqlite.go b/management/server/activity/sqlite.go index 626821fac..c46a633bb 100644 --- a/management/server/activity/sqlite.go +++ b/management/server/activity/sqlite.go @@ -11,12 +11,12 @@ import ( const ( SQLiteEventSinkDB = "events.db" createTableQuery = "CREATE TABLE IF NOT EXISTS events " + - "(id INTEGER PRIMARY KEY AUTOINCREMENT, account TEXT NOT NULL, " + - "operation INTEGER, " + - "type TEXT, " + + "(id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "activity INTEGER, " + "timestamp DATETIME, " + - "modifier TEXT," + - " target TEXT);" + "initiator_id TEXT," + + "account_id TEXT," + + " target_id TEXT);" ) // SQLiteStore is the implementation of the activity.Store interface backed by SQLite @@ -44,26 +44,23 @@ func processResult(result *sql.Rows) ([]*Event, error) { events := make([]*Event, 0) for result.Next() { var id int64 - var operation Operation + var operation Activity var timestamp time.Time - var modifier string + var initiator string var target string var account string - var typ Type - err := result.Scan(&id, &operation, ×tamp, &modifier, &target, &account, &typ) + err := result.Scan(&id, &operation, ×tamp, &initiator, &target, &account) if err != nil { return nil, err } events = append(events, &Event{ - Timestamp: timestamp, - OperationCode: operation, - Operation: MessageForOperation(operation), - ID: uint64(id), - Type: typ, - ModifierID: modifier, - TargetID: target, - AccountID: account, + Timestamp: timestamp, + Activity: operation, + ID: uint64(id), + InitiatorID: initiator, + TargetID: target, + AccountID: account, }) } @@ -76,8 +73,8 @@ func (store *SQLiteStore) Get(accountID string, offset, limit int, descending bo if !descending { order = "ASC" } - stmt, err := store.db.Prepare(fmt.Sprintf("SELECT id, operation, timestamp, modifier, target, account, type"+ - " FROM events WHERE account = ? ORDER BY timestamp %s LIMIT ? OFFSET ?;", order)) + stmt, err := store.db.Prepare(fmt.Sprintf("SELECT id, activity, timestamp, initiator_id, target_id, account_id"+ + " FROM events WHERE account_id = ? ORDER BY timestamp %s LIMIT ? OFFSET ?;", order)) if err != nil { return nil, err } @@ -94,12 +91,12 @@ 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(operation, timestamp, modifier, target, account, type) VALUES(?, ?, ?, ?, ?, ?)") + stmt, err := store.db.Prepare("INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id) VALUES(?, ?, ?, ?, ?)") if err != nil { return nil, err } - result, err := stmt.Exec(event.OperationCode, event.Timestamp, event.ModifierID, event.TargetID, event.AccountID, event.Type) + result, err := stmt.Exec(event.Activity, event.Timestamp, event.InitiatorID, event.TargetID, event.AccountID) if err != nil { return nil, err } diff --git a/management/server/activity/sqlite_test.go b/management/server/activity/sqlite_test.go index df27f782a..4fb7d76ef 100644 --- a/management/server/activity/sqlite_test.go +++ b/management/server/activity/sqlite_test.go @@ -19,12 +19,11 @@ func TestNewSQLiteStore(t *testing.T) { for i := 0; i < 10; i++ { _, err = store.Save(&Event{ - Timestamp: time.Now(), - OperationCode: AddPeerByUserOperation, - Type: ManagementEvent, - ModifierID: "user_" + fmt.Sprint(i), - TargetID: "peer_" + fmt.Sprint(i), - AccountID: accountID, + Timestamp: time.Now(), + Activity: PeerAddedByUser, + InitiatorID: "user_" + fmt.Sprint(i), + TargetID: "peer_" + fmt.Sprint(i), + AccountID: accountID, }) if err != nil { t.Fatal(err) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index e70f9ebc7..3bdba4be5 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -47,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: @@ -98,8 +98,8 @@ components: description: Peer's hostname type: string required: - - id - - name + - id + - name Peer: allOf: - $ref: '#/components/schemas/PeerMinimum' @@ -142,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: @@ -199,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: @@ -255,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' @@ -269,7 +269,7 @@ components: items: $ref: '#/components/schemas/PeerMinimum' required: - - peers + - peers PatchMinimum: type: object properties: @@ -313,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 @@ -325,7 +325,7 @@ components: description: Rule ID type: string required: - - id + - id - $ref: '#/components/schemas/RuleMinimum' - type: object properties: @@ -340,8 +340,8 @@ components: items: $ref: '#/components/schemas/GroupMinimum' required: - - sources - - destinations + - sources + - destinations RulePatchOperation: allOf: - $ref: '#/components/schemas/PatchMinimum' @@ -430,7 +430,7 @@ components: ns_type: description: Nameserver Type type: string - enum: ["udp"] + enum: [ "udp" ] port: description: Nameserver Port type: integer @@ -500,7 +500,7 @@ 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: @@ -513,16 +513,13 @@ components: description: The date and time when the event occurred type: string format: date-time - operation: - description: The operation (or action) that occurred during the event + activity: + description: The activity that occurred during the event type: string - operation_code: - description: The numeric code of the operation (or action) that occurred during the event - type: integer - type: - description: The type of the event that occurred. Indicates whether it was a management or device event + activity_code: + description: The string code of the activity that occurred during the event type: string - enum: ["device", "management"] + enum: [ "account.create", "user.join", "user.invite", "user.peer.add", "setupkey.peer.add" ] initiator_id: description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event. type: string @@ -532,33 +529,32 @@ components: required: - id - timestamp - - operation - - operation_code - - type + - activity + - activity_code - initiator_id - target_id 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 @@ -570,9 +566,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 @@ -593,7 +589,7 @@ paths: /api/users/: post: summary: Create a User (invite) - tags: [ Users] + tags: [ Users ] security: - BearerAuth: [ ] requestBody: @@ -620,7 +616,7 @@ paths: /api/users/{id}: put: summary: Update information about a User - tags: [ Users] + tags: [ Users ] security: - BearerAuth: [ ] parameters: @@ -654,9 +650,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 @@ -677,7 +673,7 @@ paths: /api/peers/{id}: get: summary: Get information about a peer - tags: [Peers] + tags: [ Peers ] security: - BearerAuth: [ ] parameters: @@ -704,7 +700,7 @@ paths: "$ref": "#/components/responses/internal_error" put: summary: Update information about a peer - tags: [Peers] + tags: [ Peers ] security: - BearerAuth: [ ] parameters: @@ -745,7 +741,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a peer - tags: [Peers] + tags: [ Peers ] security: - BearerAuth: [ ] parameters: @@ -758,7 +754,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -770,7 +766,7 @@ paths: /api/setup-keys: get: summary: Returns a list of all Setup Keys - tags: [Setup Keys] + tags: [ Setup Keys ] security: - BearerAuth: [ ] responses: @@ -792,7 +788,7 @@ paths: "$ref": "#/components/responses/internal_error" post: summary: Creates a Setup Key - tags: [Setup Keys] + tags: [ Setup Keys ] security: - BearerAuth: [ ] requestBody: @@ -819,7 +815,7 @@ paths: /api/setup-keys/{id}: get: summary: Get information about a Setup Key - tags: [Setup Keys] + tags: [ Setup Keys ] security: - BearerAuth: [ ] parameters: @@ -846,7 +842,7 @@ paths: "$ref": "#/components/responses/internal_error" put: summary: Update information about a Setup Key - tags: [Setup Keys] + tags: [ Setup Keys ] security: - BearerAuth: [ ] parameters: @@ -879,7 +875,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Setup Key - tags: [Setup Keys] + tags: [ Setup Keys ] security: - BearerAuth: [ ] parameters: @@ -892,7 +888,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -904,7 +900,7 @@ paths: /api/groups: get: summary: Returns a list of all Groups - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] responses: @@ -926,7 +922,7 @@ paths: "$ref": "#/components/responses/internal_error" post: summary: Creates a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] requestBody: @@ -962,7 +958,7 @@ paths: /api/groups/{id}: get: summary: Get information about a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] parameters: @@ -989,7 +985,7 @@ paths: "$ref": "#/components/responses/internal_error" put: summary: Update/Replace a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] parameters: @@ -1064,7 +1060,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Group - tags: [Groups] + tags: [ Groups ] security: - BearerAuth: [ ] parameters: @@ -1077,7 +1073,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': @@ -1089,7 +1085,7 @@ paths: /api/rules: get: summary: Returns a list of all Rules - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] responses: @@ -1111,7 +1107,7 @@ paths: "$ref": "#/components/responses/internal_error" post: summary: Creates a Rule - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] requestBody: @@ -1141,7 +1137,7 @@ paths: /api/rules/{id}: get: summary: Get information about a Rules - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] parameters: @@ -1168,7 +1164,7 @@ paths: "$ref": "#/components/responses/internal_error" put: summary: Update/Replace a Rule - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] parameters: @@ -1247,7 +1243,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Rule - tags: [Rules] + tags: [ Rules ] security: - BearerAuth: [ ] parameters: @@ -1260,7 +1256,7 @@ paths: responses: '200': description: Delete status code - content: {} + content: { } '400': "$ref": "#/components/responses/bad_request" '401': diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index a4b82d3b6..99f929a6a 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -11,10 +11,13 @@ const ( BearerAuthScopes = "BearerAuth.Scopes" ) -// Defines values for EventType. +// Defines values for EventActivityCode. const ( - EventTypeDevice EventType = "device" - EventTypeManagement EventType = "management" + EventActivityCodeAccountCreate EventActivityCode = "account.create" + EventActivityCodeSetupkeyPeerAdd EventActivityCode = "setupkey.peer.add" + EventActivityCodeUserInvite EventActivityCode = "user.invite" + EventActivityCodeUserJoin EventActivityCode = "user.join" + EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add" ) // Defines values for GroupPatchOperationOp. @@ -105,30 +108,27 @@ const ( // 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"` - // Operation The operation (or action) that occurred during the event - Operation string `json:"operation"` - - // OperationCode The numeric code of the operation (or action) that occurred during the event - OperationCode int `json:"operation_code"` - // 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"` - - // Type The type of the event that occurred. Indicates whether it was a management or device event - Type EventType `json:"type"` } -// EventType The type of the event that occurred. Indicates whether it was a management or device event -type EventType string +// EventActivityCode The string code of the activity that occurred during the event +type EventActivityCode string // Group defines model for Group. type Group struct { diff --git a/management/server/http/events.go b/management/server/http/events.go index e3f88c983..af332af4d 100644 --- a/management/server/http/events.go +++ b/management/server/http/events.go @@ -42,7 +42,7 @@ func (h *Events) GetEvents(w http.ResponseWriter, r *http.Request) { util.WriteError(err, w) return } - var events []*api.Event + events := make([]*api.Event, 0) for _, e := range accountEvents { events = append(events, toEventResponse(e)) } @@ -53,12 +53,11 @@ func (h *Events) GetEvents(w http.ResponseWriter, r *http.Request) { func toEventResponse(event *activity.Event) *api.Event { return &api.Event{ - Id: fmt.Sprint(event.ID), - InitiatorId: event.ModifierID, - Operation: event.Operation, - OperationCode: int(event.OperationCode), - TargetId: event.TargetID, - Timestamp: event.Timestamp, - Type: api.EventType(event.Type), + Id: fmt.Sprint(event.ID), + InitiatorId: event.InitiatorID, + Activity: event.Activity.Message(), + ActivityCode: api.EventActivityCode(event.Activity.StringCode()), + TargetId: event.TargetID, + Timestamp: event.Timestamp, } } diff --git a/management/server/peer.go b/management/server/peer.go index a3ade59a6..268ba0d16 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -363,7 +363,6 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* opEvent := &activity.Event{ Timestamp: time.Now(), - Type: activity.ManagementEvent, AccountID: account.Id, } @@ -379,11 +378,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* } account.SetupKeys[sk.Key] = sk.IncrementUsage() - opEvent.ModifierID = sk.Id - opEvent.OperationCode = activity.AddPeerWithKeyOperation + opEvent.InitiatorID = sk.Id + opEvent.Activity = activity.PeerAddedWithSetupKey } else { - opEvent.ModifierID = userID - opEvent.OperationCode = activity.AddPeerByUserOperation + opEvent.InitiatorID = userID + opEvent.Activity = activity.PeerAddedByUser } takenIps := account.getTakenIPs() diff --git a/management/server/user.go b/management/server/user.go index 82775c726..17ba808a1 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -3,10 +3,9 @@ package server import ( "fmt" "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" ) const ( From 3d6b0e36384a0c1d87348c8531d9980ae05c1c49 Mon Sep 17 00:00:00 2001 From: braginini Date: Mon, 12 Dec 2022 14:05:11 +0100 Subject: [PATCH 25/30] Add user invite event --- management/server/account.go | 2 +- management/server/account_test.go | 7 ++++++- management/server/activity/event.go | 2 +- management/server/http/users.go | 4 ++-- management/server/mock_server/account_mock.go | 18 ++++++++++++++---- management/server/user.go | 17 ++++++++++++++++- 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 3049dcafe..9b23e853b 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -42,7 +42,7 @@ type AccountManager interface { 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) + 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) diff --git a/management/server/account_test.go b/management/server/account_test.go index 31480eb5d..80052875d 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" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/route" "net" "reflect" @@ -1228,7 +1229,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 index 85fc4d3a5..f20d4cfe8 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -17,7 +17,7 @@ const ( const ( // PeerAddedByUserMessage is a human-readable text message of the PeerAddedByUser activity - PeerAddedByUserMessage string = "New peer added by a user" + PeerAddedByUserMessage string = "User added a new peer" // PeerAddedWithSetupKeyMessage is a human-readable text message of the PeerAddedWithSetupKey activity PeerAddedWithSetupKeyMessage = "New peer added with a setup key" //UserJoinedMessage is a human-readable text message of the UserJoined activity diff --git a/management/server/http/users.go b/management/server/http/users.go index 698bb9410..7772e4f0f 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/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 8efeee33a..b50576fb5 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" @@ -58,9 +59,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 @@ -456,9 +458,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 +479,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 +489,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/user.go b/management/server/user.go index 17ba808a1..9e8a3fa59 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -2,10 +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" + "time" ) const ( @@ -116,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() @@ -175,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 ca291a0fd6720bb226ac85d0e6fe8161bb72fa2d Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 13 Dec 2022 15:50:45 +0100 Subject: [PATCH 26/30] Remove duplicate UserJoined events from the response --- management/server/account.go | 11 +++++++++++ management/server/event.go | 29 +++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 9b23e853b..f9c0a6ee2 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -853,6 +853,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/event.go b/management/server/event.go index 7f7b59583..3c52d94b3 100644 --- a/management/server/event.go +++ b/management/server/event.go @@ -1,8 +1,33 @@ package server -import "github.com/netbirdio/netbird/management/server/activity" +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) { - return am.eventStore.Get(accountID, 0, 1000, true) + 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 } From f485b654b43f6ad977555e6aff20ceda91f92378 Mon Sep 17 00:00:00 2001 From: braginini Date: Tue, 20 Dec 2022 18:52:18 +0100 Subject: [PATCH 27/30] Add more events --- management/server/account.go | 6 +-- management/server/activity/event.go | 49 +++++++++++++++++++++-- management/server/activity/sqlite.go | 29 ++++++++++++-- management/server/activity/sqlite_test.go | 22 +++++----- management/server/grpcserver.go | 10 +---- management/server/http/api/openapi.yml | 8 +++- management/server/http/api/types.gen.go | 7 ++++ management/server/http/events.go | 8 +++- management/server/http/peers.go | 15 +++---- management/server/http/rules.go | 12 +++--- management/server/peer.go | 29 +++++++++++++- management/server/rule.go | 48 +++++++++++++++++++++- 12 files changed, 194 insertions(+), 49 deletions(-) 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) } From ef171ebdd4577727b45b59ed32e79d60184373f0 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 21 Dec 2022 18:43:10 +0100 Subject: [PATCH 28/30] Add setup key events --- management/server/account.go | 4 +-- management/server/activity/event.go | 32 +++++++++++++++++ management/server/http/api/openapi.yml | 6 +++- management/server/http/api/types.gen.go | 4 +++ management/server/http/setupkeys.go | 8 ++--- management/server/setupkey.go | 48 +++++++++++++++++++++---- 6 files changed, 88 insertions(+), 14 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 90ab2108f..7facdaa15 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -40,8 +40,8 @@ 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) + 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) diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 1584abe11..14603af99 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -21,6 +21,14 @@ const ( 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 ) const ( @@ -42,6 +50,14 @@ const ( 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" ) // Activity that triggered an Event @@ -68,6 +84,14 @@ func (a Activity) Message() string { return RuleRemovedMessage case RuleUpdated: return RuleUpdatedMessage + case SetupKeyCreated: + return SetupKeyCreatedMessage + case SetupKeyUpdated: + return SetupKeyUpdatedMessage + case SetupKeyRevoked: + return SetupKeyRevokedMessage + case SetupKeyOverused: + return SetupKeyOverusedMessage default: return "UNKNOWN_ACTIVITY" } @@ -94,6 +118,14 @@ func (a Activity) StringCode() string { 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" default: return "UNKNOWN_ACTIVITY" } diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 238a74f49..71e6842d1 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -519,7 +519,11 @@ 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", "user.peer.delete", "rule.add", "rule.delete", "rule.update"] + 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", + "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 diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 3c99a7948..e723e37fc 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -17,7 +17,11 @@ const ( 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" diff --git a/management/server/http/setupkeys.go b/management/server/http/setupkeys.go index a88cba29e..cecc86036 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/setupkey.go b/management/server/setupkey.go index 824d381a2..84fe45d71 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 peer +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 From a32bc441d283c8b16e4d31d69dce29b91d326931 Mon Sep 17 00:00:00 2001 From: braginini Date: Wed, 21 Dec 2022 18:55:14 +0100 Subject: [PATCH 29/30] Add group events --- management/server/account.go | 2 +- management/server/activity/event.go | 16 ++++++++++++++ management/server/group.go | 29 ++++++++++++++++++++++--- management/server/http/api/openapi.yml | 1 + management/server/http/api/types.gen.go | 2 ++ management/server/http/groups.go | 8 +++---- management/server/setupkey.go | 2 +- 7 files changed, 51 insertions(+), 9 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 7facdaa15..1b4768fd3 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -63,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) diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 14603af99..34b7ad040 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -29,6 +29,10 @@ const ( 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 ) const ( @@ -58,6 +62,10 @@ const ( 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" ) // Activity that triggered an Event @@ -92,6 +100,10 @@ func (a Activity) Message() string { return SetupKeyRevokedMessage case SetupKeyOverused: return SetupKeyOverusedMessage + case GroupCreated: + return GroupCreatedMessage + case GroupUpdated: + return GroupUpdatedMessage default: return "UNKNOWN_ACTIVITY" } @@ -126,6 +138,10 @@ func (a Activity) StringCode() string { return "setupkey.overuse" case SetupKeyUpdated: return "setupkey.update" + case GroupCreated: + return "group.add" + case GroupUpdated: + return "group.update" default: return "UNKNOWN_ACTIVITY" } diff --git a/management/server/group.go b/management/server/group.go index cfc784487..8455ab4ea 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/http/api/openapi.yml b/management/server/http/api/openapi.yml index 71e6842d1..f452cfb68 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", "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: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index e723e37fc..e180265d0 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -14,6 +14,8 @@ const ( // 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" diff --git a/management/server/http/groups.go b/management/server/http/groups.go index a636da375..8ee4f6bae 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/setupkey.go b/management/server/setupkey.go index 84fe45d71..5df434db8 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -108,7 +108,7 @@ func (key *SetupKey) Copy() *SetupKey { } } -// EventMeta returns activity event meta related to the peer +// 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} } From 359b2a13a711ee2d9328ef72e5a0e6b89088a935 Mon Sep 17 00:00:00 2001 From: braginini Date: Thu, 22 Dec 2022 11:54:26 +0100 Subject: [PATCH 30/30] Make test compile --- management/server/account_test.go | 18 ++++++++++-------- management/server/activity/event.go | 8 ++++++++ management/server/management_proto_test.go | 8 +++++++- management/server/management_test.go | 8 +++++++- management/server/nameserver_test.go | 7 ++++++- management/server/peer_test.go | 18 +++++++++--------- management/server/route_test.go | 20 ++++++++++++-------- management/server/setupkey_test.go | 12 ++++++------ 8 files changed, 65 insertions(+), 34 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 80052875d..23afd345b 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -633,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) } @@ -715,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 } @@ -740,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, userID, defaultRule.ID); err != nil { t.Errorf("delete default rule: %v", err) return } @@ -760,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 } @@ -780,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, userID, peer3.Key); err != nil { t.Errorf("delete peer: %v", err) return } @@ -815,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) } @@ -844,7 +846,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { return } - _, err = manager.DeletePeer(account.Id, peerKey) + _, err = manager.DeletePeer(account.Id, userID, peerKey) if err != nil { return } diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 34b7ad040..d92698863 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -33,6 +33,8 @@ const ( GroupCreated // GroupUpdated indicates that a user updated a group GroupUpdated + // PeerGroupsUpdated indicates that a user updated groups of a peer + PeerGroupsUpdated ) const ( @@ -66,6 +68,8 @@ 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" ) // Activity that triggered an Event @@ -104,6 +108,8 @@ func (a Activity) Message() string { return GroupCreatedMessage case GroupUpdated: return GroupUpdatedMessage + case PeerGroupsUpdated: + return PeerGroupsUpdatedMessage default: return "UNKNOWN_ACTIVITY" } @@ -142,6 +148,8 @@ func (a Activity) StringCode() string { return "group.add" case GroupUpdated: return "group.update" + case PeerGroupsUpdated: + return "peer.groups.update" default: return "UNKNOWN_ACTIVITY" } diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 5dc20b0cb..bace763fd 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + "github.com/netbirdio/netbird/management/server/activity" "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 f4c1b2ac0..093afcc18 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -2,6 +2,7 @@ package server_test import ( "context" + "github.com/netbirdio/netbird/management/server/activity" "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/nameserver_test.go b/management/server/nameserver_test.go index 30cc8246f..b419080af 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" + "github.com/netbirdio/netbird/management/server/activity" "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_test.go b/management/server/peer_test.go index 54f4629ba..28242cddc 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 1bd0598cc..87c1325cb 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1,6 +1,7 @@ package server import ( + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/route" "github.com/rs/xid" "github.com/stretchr/testify/require" @@ -14,6 +15,7 @@ const ( routeGroup1 = "routeGroup1" routeGroup2 = "routeGroup2" routeInvalidGroup1 = "routeInvalidGroup1" + userID = "testingUser" ) func TestCreateRoute(t *testing.T) { @@ -831,7 +833,6 @@ func TestDeleteRoute(t *testing.T) { func TestGetNetworkMap_RouteSync(t *testing.T) { // no routes for peer in different groups // no routes when route is deleted - baseRoute := &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -895,7 +896,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { Name: "peer1 group", Peers: []string{peer1Key}, } - err = am.SaveGroup(account.Id, newGroup) + err = am.SaveGroup(account.Id, userID, newGroup) require.NoError(t, err) rules, err := am.ListRules(account.Id, "testingUser") @@ -908,10 +909,10 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { newRule.Source = []string{newGroup.ID} newRule.Destination = []string{newGroup.ID} - err = am.SaveRule(account.Id, newRule) + err = am.SaveRule(account.Id, userID, newRule) require.NoError(t, err) - err = am.DeleteRule(account.Id, defaultRule.ID) + err = am.DeleteRule(account.Id, userID, defaultRule.ID) 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/setupkey_test.go b/management/server/setupkey_test.go index 290259116..41b3c1697 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 {