mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-12 09:50:47 +01:00
d4b6d7646c
Implement user deletion across all IDP-ss. Expires all user peers when the user is deleted. Users are permanently removed from a local store, but in IDP, we remove Netbird attributes for the user untilUserDeleteFromIDPEnabled setting is not enabled. To test, an admin user should remove any additional users. Until the UI incorporates this feature, use a curl DELETE request targeting the /users/<USER_ID> management endpoint. Note that this request only removes user attributes and doesn't trigger a delete from the IDP. To enable user removal from the IdP, set UserDeleteFromIDPEnabled to true in account settings. Until we have a UI for this, make this change directly in the store file. Store the deleted email addresses in encrypted in activity store.
252 lines
6.3 KiB
Go
252 lines
6.3 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3" // sqlite driver
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
|
)
|
|
|
|
const (
|
|
//eventSinkDB is the default name of the events database
|
|
eventSinkDB = "events.db"
|
|
createTableQuery = "CREATE TABLE IF NOT EXISTS events " +
|
|
"(id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"activity INTEGER, " +
|
|
"timestamp DATETIME, " +
|
|
"initiator_id TEXT," +
|
|
"account_id TEXT," +
|
|
"meta TEXT," +
|
|
" target_id TEXT);"
|
|
|
|
creatTableAccountEmailQuery = `CREATE TABLE IF NOT EXISTS deleted_users (id TEXT NOT NULL, email TEXT NOT NULL);`
|
|
|
|
selectDescQuery = `SELECT events.id, activity, timestamp, initiator_id, i.email as "initiator_email", target_id, t.email as "target_email", account_id, meta
|
|
FROM events
|
|
LEFT JOIN deleted_users i ON events.initiator_id = i.id
|
|
LEFT JOIN deleted_users t ON events.target_id = t.id
|
|
WHERE account_id = ?
|
|
ORDER BY timestamp DESC LIMIT ? OFFSET ?;`
|
|
|
|
selectAscQuery = `SELECT events.id, activity, timestamp, initiator_id, i.email as "initiator_email", target_id, t.email as "target_email", account_id, meta
|
|
FROM events
|
|
LEFT JOIN deleted_users i ON events.initiator_id = i.id
|
|
LEFT JOIN deleted_users t ON events.target_id = t.id
|
|
WHERE account_id = ?
|
|
ORDER BY timestamp ASC LIMIT ? OFFSET ?;`
|
|
|
|
insertQuery = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " +
|
|
"VALUES(?, ?, ?, ?, ?, ?)"
|
|
|
|
insertDeleteUserQuery = `INSERT INTO deleted_users(id, email) VALUES(?, ?)`
|
|
)
|
|
|
|
// Store is the implementation of the activity.Store interface backed by SQLite
|
|
type Store struct {
|
|
db *sql.DB
|
|
emailEncrypt *EmailEncrypt
|
|
|
|
insertStatement *sql.Stmt
|
|
selectAscStatement *sql.Stmt
|
|
selectDescStatement *sql.Stmt
|
|
deleteUserStmt *sql.Stmt
|
|
}
|
|
|
|
// NewSQLiteStore creates a new Store with an event table if not exists.
|
|
func NewSQLiteStore(dataDir string, encryptionKey string) (*Store, error) {
|
|
dbFile := filepath.Join(dataDir, eventSinkDB)
|
|
db, err := sql.Open("sqlite3", dbFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
crypt, err := NewEmailEncrypt(encryptionKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = db.Exec(createTableQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = db.Exec(creatTableAccountEmailQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
insertStmt, err := db.Prepare(insertQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
selectDescStmt, err := db.Prepare(selectDescQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
selectAscStmt, err := db.Prepare(selectAscQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deleteUserStmt, err := db.Prepare(insertDeleteUserQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &Store{
|
|
db: db,
|
|
emailEncrypt: crypt,
|
|
insertStatement: insertStmt,
|
|
selectDescStatement: selectDescStmt,
|
|
selectAscStatement: selectAscStmt,
|
|
deleteUserStmt: deleteUserStmt,
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
|
events := make([]*activity.Event, 0)
|
|
for result.Next() {
|
|
var id int64
|
|
var operation activity.Activity
|
|
var timestamp time.Time
|
|
var initiator string
|
|
var initiatorEmail *string
|
|
var target string
|
|
var targetEmail *string
|
|
var account string
|
|
var jsonMeta string
|
|
err := result.Scan(&id, &operation, ×tamp, &initiator, &initiatorEmail, &target, &targetEmail, &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
|
|
}
|
|
}
|
|
|
|
if targetEmail != nil {
|
|
email, err := store.emailEncrypt.Decrypt(*targetEmail)
|
|
if err != nil {
|
|
log.Errorf("failed to decrypt email address for target id: %s", target)
|
|
meta["email"] = ""
|
|
} else {
|
|
meta["email"] = email
|
|
}
|
|
}
|
|
|
|
event := &activity.Event{
|
|
Timestamp: timestamp,
|
|
Activity: operation,
|
|
ID: uint64(id),
|
|
InitiatorID: initiator,
|
|
TargetID: target,
|
|
AccountID: account,
|
|
Meta: meta,
|
|
}
|
|
|
|
if initiatorEmail != nil {
|
|
email, err := store.emailEncrypt.Decrypt(*initiatorEmail)
|
|
if err != nil {
|
|
log.Errorf("failed to decrypt email address of initiator: %s", initiator)
|
|
} else {
|
|
event.InitiatorEmail = email
|
|
}
|
|
}
|
|
|
|
events = append(events, event)
|
|
}
|
|
|
|
return events, nil
|
|
}
|
|
|
|
// Get returns "limit" number of events from index ordered descending or ascending by a timestamp
|
|
func (store *Store) Get(accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {
|
|
stmt := store.selectDescStatement
|
|
if !descending {
|
|
stmt = store.selectAscStatement
|
|
}
|
|
|
|
result, err := stmt.Query(accountID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer result.Close() //nolint
|
|
return store.processResult(result)
|
|
}
|
|
|
|
// Save an event in the SQLite events table end encrypt the "email" element in meta map
|
|
func (store *Store) Save(event *activity.Event) (*activity.Event, error) {
|
|
var jsonMeta string
|
|
meta, err := store.saveDeletedUserEmailInEncrypted(event)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if meta != nil {
|
|
metaBytes, err := json.Marshal(event.Meta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
jsonMeta = string(metaBytes)
|
|
}
|
|
|
|
result, err := store.insertStatement.Exec(event.Activity, event.Timestamp, event.InitiatorID, event.TargetID, event.AccountID, jsonMeta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
eventCopy := event.Copy()
|
|
eventCopy.ID = uint64(id)
|
|
return eventCopy, nil
|
|
}
|
|
|
|
// saveDeletedUserEmailInEncrypted if the meta contains email then store it in encrypted way and delete this item from
|
|
// meta map
|
|
func (store *Store) saveDeletedUserEmailInEncrypted(event *activity.Event) (map[string]any, error) {
|
|
email, ok := event.Meta["email"]
|
|
if !ok {
|
|
return event.Meta, nil
|
|
}
|
|
|
|
delete(event.Meta, "email")
|
|
|
|
encrypted := store.emailEncrypt.Encrypt(fmt.Sprintf("%s", email))
|
|
_, err := store.deleteUserStmt.Exec(event.TargetID, encrypted)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(event.Meta) == 1 {
|
|
return nil, nil // nolint
|
|
}
|
|
delete(event.Meta, "email")
|
|
return event.Meta, nil
|
|
}
|
|
|
|
// Close the Store
|
|
func (store *Store) Close() error {
|
|
if store.db != nil {
|
|
return store.db.Close()
|
|
}
|
|
return nil
|
|
}
|