mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-07 08:44:07 +01:00
Handle user delete (#1113)
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.
This commit is contained in:
parent
8febab4076
commit
d4b6d7646c
@ -76,7 +76,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
|
||||
return nil, nil
|
||||
}
|
||||
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||
eventStore)
|
||||
eventStore, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -1049,7 +1049,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) {
|
||||
return nil, "", err
|
||||
}
|
||||
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||
eventStore)
|
||||
eventStore, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
||||
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||
eventStore)
|
||||
eventStore, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
"github.com/netbirdio/netbird/encryption"
|
||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/activity/sqlite"
|
||||
httpapi "github.com/netbirdio/netbird/management/server/http"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
@ -142,12 +143,22 @@ var (
|
||||
if disableSingleAccMode {
|
||||
mgmtSingleAccModeDomain = ""
|
||||
}
|
||||
eventStore, err := sqlite.NewSQLiteStore(config.Datadir)
|
||||
eventStore, key, err := initEventStore(config.Datadir, config.DataStoreEncryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to initialize database: %s", err)
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
log.Debugf("update config with activity store key")
|
||||
config.DataStoreEncryptionKey = key
|
||||
err := updateMgmtConfig(mgmtConfig, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write out store encryption key: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain,
|
||||
dnsDomain, eventStore)
|
||||
dnsDomain, eventStore, userDeleteFromIDPEnabled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build default manager: %v", err)
|
||||
}
|
||||
@ -287,6 +298,20 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func initEventStore(dataDir string, key string) (activity.Store, string, error) {
|
||||
var err error
|
||||
if key == "" {
|
||||
log.Debugf("generate new activity store encryption key")
|
||||
key, err = sqlite.GenerateKey()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
store, err := sqlite.NewSQLiteStore(dataDir, key)
|
||||
return store, key, err
|
||||
|
||||
}
|
||||
|
||||
func notifyStop(msg string) {
|
||||
select {
|
||||
case stopCh <- 1:
|
||||
@ -440,6 +465,10 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
||||
return loadedConfig, err
|
||||
}
|
||||
|
||||
func updateMgmtConfig(path string, config *server.Config) error {
|
||||
return util.WriteJson(path, config)
|
||||
}
|
||||
|
||||
// OIDCConfigResponse used for parsing OIDC config response
|
||||
type OIDCConfigResponse struct {
|
||||
Issuer string `json:"issuer"`
|
||||
|
@ -24,6 +24,7 @@ var (
|
||||
disableMetrics bool
|
||||
disableSingleAccMode bool
|
||||
idpSignKeyRefreshEnabled bool
|
||||
userDeleteFromIDPEnabled bool
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "netbird-mgmt",
|
||||
@ -56,6 +57,7 @@ func init() {
|
||||
mgmtCmd.Flags().BoolVar(&disableMetrics, "disable-anonymous-metrics", false, "disables push of anonymous usage metrics to NetBird")
|
||||
mgmtCmd.Flags().StringVar(&dnsDomain, "dns-domain", defaultSingleAccModeDomain, fmt.Sprintf("Domain used for peer resolution. This is appended to the peer's name, e.g. pi-server. %s. Max lenght is 192 characters to allow appending to a peer name with up to 63 characters.", defaultSingleAccModeDomain))
|
||||
mgmtCmd.Flags().BoolVar(&idpSignKeyRefreshEnabled, "idp-sign-key-refresh-enabled", false, "Enable cache headers evaluation to determine signing key rotation period. This will refresh the signing key upon expiry.")
|
||||
mgmtCmd.Flags().BoolVar(&userDeleteFromIDPEnabled, "user-delete-from-idp", false, "Allows to delete user from IDP when user is deleted from account")
|
||||
rootCmd.MarkFlagRequired("config") //nolint
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "")
|
||||
|
@ -130,6 +130,9 @@ type DefaultAccountManager struct {
|
||||
// dnsDomain is used for peer resolution. This is appended to the peer's name
|
||||
dnsDomain string
|
||||
peerLoginExpiry Scheduler
|
||||
|
||||
// userDeleteFromIDPEnabled allows to delete user from IDP when user is deleted from account
|
||||
userDeleteFromIDPEnabled bool
|
||||
}
|
||||
|
||||
// Settings represents Account settings structure that can be modified via API and Dashboard
|
||||
@ -735,18 +738,19 @@ func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) {
|
||||
|
||||
// BuildManager creates a new DefaultAccountManager with a provided Store
|
||||
func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager,
|
||||
singleAccountModeDomain string, dnsDomain string, eventStore activity.Store,
|
||||
singleAccountModeDomain string, dnsDomain string, eventStore activity.Store, userDeleteFromIDPEnabled bool,
|
||||
) (*DefaultAccountManager, error) {
|
||||
am := &DefaultAccountManager{
|
||||
Store: store,
|
||||
peersUpdateManager: peersUpdateManager,
|
||||
idpManager: idpManager,
|
||||
ctx: context.Background(),
|
||||
cacheMux: sync.Mutex{},
|
||||
cacheLoading: map[string]chan struct{}{},
|
||||
dnsDomain: dnsDomain,
|
||||
eventStore: eventStore,
|
||||
peerLoginExpiry: NewDefaultScheduler(),
|
||||
Store: store,
|
||||
peersUpdateManager: peersUpdateManager,
|
||||
idpManager: idpManager,
|
||||
ctx: context.Background(),
|
||||
cacheMux: sync.Mutex{},
|
||||
cacheLoading: map[string]chan struct{}{},
|
||||
dnsDomain: dnsDomain,
|
||||
eventStore: eventStore,
|
||||
peerLoginExpiry: NewDefaultScheduler(),
|
||||
userDeleteFromIDPEnabled: userDeleteFromIDPEnabled,
|
||||
}
|
||||
allAccounts := store.GetAllAccounts()
|
||||
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
|
||||
@ -871,33 +875,19 @@ func (am *DefaultAccountManager) peerLoginExpirationJob(accountID string) func()
|
||||
return account.GetNextPeerExpiration()
|
||||
}
|
||||
|
||||
expiredPeers := account.GetExpiredPeers()
|
||||
var peerIDs []string
|
||||
for _, peer := range account.GetExpiredPeers() {
|
||||
if peer.Status.LoginExpired {
|
||||
continue
|
||||
}
|
||||
for _, peer := range expiredPeers {
|
||||
peerIDs = append(peerIDs, peer.ID)
|
||||
peer.MarkLoginExpired(true)
|
||||
account.UpdatePeer(peer)
|
||||
err = am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status)
|
||||
if err != nil {
|
||||
log.Errorf("failed saving peer status while expiring peer %s", peer.ID)
|
||||
return account.GetNextPeerExpiration()
|
||||
}
|
||||
am.storeEvent(peer.UserID, peer.ID, account.Id, activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()))
|
||||
}
|
||||
|
||||
log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id)
|
||||
|
||||
if len(peerIDs) != 0 {
|
||||
// this will trigger peer disconnect from the management service
|
||||
am.peersUpdateManager.CloseChannels(peerIDs)
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
log.Errorf("failed updating account peers while expiring peers for account %s", accountID)
|
||||
return account.GetNextPeerExpiration()
|
||||
}
|
||||
if err := am.expireAndUpdatePeers(account, expiredPeers); err != nil {
|
||||
log.Errorf("failed updating account peers while expiring peers for account %s", account.Id)
|
||||
return account.GetNextPeerExpiration()
|
||||
}
|
||||
|
||||
return account.GetNextPeerExpiration()
|
||||
}
|
||||
}
|
||||
|
@ -2063,7 +2063,7 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
return nil, err
|
||||
}
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore)
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore, false)
|
||||
}
|
||||
|
||||
func createStore(t *testing.T) (Store, error) {
|
||||
|
@ -104,6 +104,8 @@ const (
|
||||
UserBlocked
|
||||
// UserUnblocked indicates that a user unblocked another user
|
||||
UserUnblocked
|
||||
// UserDeleted indicates that a user deleted another user
|
||||
UserDeleted
|
||||
// GroupDeleted indicates that a user deleted group
|
||||
GroupDeleted
|
||||
// UserLoggedInPeer indicates that user logged in their peer with an interactive SSO login
|
||||
@ -162,6 +164,7 @@ var activityMap = map[Activity]Code{
|
||||
ServiceUserDeleted: {"Service user deleted", "service.user.delete"},
|
||||
UserBlocked: {"User blocked", "user.block"},
|
||||
UserUnblocked: {"User unblocked", "user.unblock"},
|
||||
UserDeleted: {"User deleted", "user.delete"},
|
||||
GroupDeleted: {"Group deleted", "group.delete"},
|
||||
UserLoggedInPeer: {"User logged in peer", "user.peer.login"},
|
||||
PeerLoginExpired: {"Peer login expired", "peer.login.expire"},
|
||||
|
@ -18,10 +18,13 @@ type Event struct {
|
||||
ID uint64
|
||||
// InitiatorID is the ID of an object that initiated the event (e.g., a user)
|
||||
InitiatorID string
|
||||
// InitiatorEmail is the email address of an object that initiated the event. This will be set on deleted users only
|
||||
InitiatorEmail string
|
||||
// TargetID is the ID of an object that was effected by the event (e.g., a peer)
|
||||
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
|
||||
}
|
||||
@ -35,12 +38,13 @@ func (e *Event) Copy() *Event {
|
||||
}
|
||||
|
||||
return &Event{
|
||||
Timestamp: e.Timestamp,
|
||||
Activity: e.Activity,
|
||||
ID: e.ID,
|
||||
InitiatorID: e.InitiatorID,
|
||||
TargetID: e.TargetID,
|
||||
AccountID: e.AccountID,
|
||||
Meta: meta,
|
||||
Timestamp: e.Timestamp,
|
||||
Activity: e.Activity,
|
||||
ID: e.ID,
|
||||
InitiatorID: e.InitiatorID,
|
||||
InitiatorEmail: e.InitiatorEmail,
|
||||
TargetID: e.TargetID,
|
||||
AccountID: e.AccountID,
|
||||
Meta: meta,
|
||||
}
|
||||
}
|
||||
|
81
management/server/activity/sqlite/crypt.go
Normal file
81
management/server/activity/sqlite/crypt.go
Normal file
@ -0,0 +1,81 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var iv = []byte{10, 22, 13, 79, 05, 8, 52, 91, 87, 98, 88, 98, 35, 25, 13, 05}
|
||||
|
||||
type EmailEncrypt struct {
|
||||
block cipher.Block
|
||||
}
|
||||
|
||||
func GenerateKey() (string, error) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
readableKey := base64.StdEncoding.EncodeToString(key)
|
||||
return readableKey, nil
|
||||
}
|
||||
|
||||
func NewEmailEncrypt(key string) (*EmailEncrypt, error) {
|
||||
binKey, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(binKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ec := &EmailEncrypt{
|
||||
block: block,
|
||||
}
|
||||
|
||||
return ec, nil
|
||||
}
|
||||
|
||||
func (ec *EmailEncrypt) Encrypt(payload string) string {
|
||||
plainText := pkcs5Padding([]byte(payload))
|
||||
cipherText := make([]byte, len(plainText))
|
||||
cbc := cipher.NewCBCEncrypter(ec.block, iv)
|
||||
cbc.CryptBlocks(cipherText, plainText)
|
||||
return base64.StdEncoding.EncodeToString(cipherText)
|
||||
}
|
||||
|
||||
func (ec *EmailEncrypt) Decrypt(data string) (string, error) {
|
||||
cipherText, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cbc := cipher.NewCBCDecrypter(ec.block, iv)
|
||||
cbc.CryptBlocks(cipherText, cipherText)
|
||||
payload, err := pkcs5UnPadding(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(payload), nil
|
||||
}
|
||||
|
||||
func pkcs5Padding(ciphertext []byte) []byte {
|
||||
padding := aes.BlockSize - len(ciphertext)%aes.BlockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padText...)
|
||||
}
|
||||
|
||||
func pkcs5UnPadding(src []byte) ([]byte, error) {
|
||||
srcLen := len(src)
|
||||
paddingLen := int(src[srcLen-1])
|
||||
if paddingLen >= srcLen || paddingLen > aes.BlockSize {
|
||||
return nil, fmt.Errorf("padding size error")
|
||||
}
|
||||
return src[:srcLen-paddingLen], nil
|
||||
}
|
63
management/server/activity/sqlite/crypt_test.go
Normal file
63
management/server/activity/sqlite/crypt_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateKey(t *testing.T) {
|
||||
testData := "exampl@netbird.io"
|
||||
key, err := GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %s", err)
|
||||
}
|
||||
ee, err := NewEmailEncrypt(key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to init email encryption: %s", err)
|
||||
}
|
||||
|
||||
encrypted := ee.Encrypt(testData)
|
||||
if encrypted == "" {
|
||||
t.Fatalf("invalid encrypted text")
|
||||
}
|
||||
|
||||
decrypted, err := ee.Decrypt(encrypted)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decrypt data: %s", err)
|
||||
}
|
||||
|
||||
if decrypted != testData {
|
||||
t.Fatalf("decrypted data is not match with test data: %s, %s", testData, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorruptKey(t *testing.T) {
|
||||
testData := "exampl@netbird.io"
|
||||
key, err := GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %s", err)
|
||||
}
|
||||
ee, err := NewEmailEncrypt(key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to init email encryption: %s", err)
|
||||
}
|
||||
|
||||
encrypted := ee.Encrypt(testData)
|
||||
if encrypted == "" {
|
||||
t.Fatalf("invalid encrypted text")
|
||||
}
|
||||
|
||||
newKey, err := GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %s", err)
|
||||
}
|
||||
|
||||
ee, err = NewEmailEncrypt(newKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to init email encryption: %s", err)
|
||||
}
|
||||
|
||||
res, err := ee.Decrypt(encrypted)
|
||||
if err == nil || res == testData {
|
||||
t.Fatalf("incorrect decryption, the result is: %s", res)
|
||||
}
|
||||
}
|
@ -3,14 +3,14 @@ package sqlite
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
|
||||
// sqlite driver
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
_ "github.com/mattn/go-sqlite3" // sqlite driver
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -25,35 +25,62 @@ const (
|
||||
"meta TEXT," +
|
||||
" target_id TEXT);"
|
||||
|
||||
selectDescQuery = "SELECT id, activity, timestamp, initiator_id, target_id, account_id, meta" +
|
||||
" FROM events WHERE account_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?;"
|
||||
selectAscQuery = "SELECT id, activity, timestamp, initiator_id, target_id, account_id, meta" +
|
||||
" FROM events WHERE account_id = ? ORDER BY timestamp ASC LIMIT ? OFFSET ?;"
|
||||
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
|
||||
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) (*Store, error) {
|
||||
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
|
||||
@ -69,25 +96,35 @@ func NewSQLiteStore(dataDir string) (*Store, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Store{
|
||||
deleteUserStmt, err := db.Prepare(insertDeleteUserQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Store{
|
||||
db: db,
|
||||
emailEncrypt: crypt,
|
||||
insertStatement: insertStmt,
|
||||
selectDescStatement: selectDescStmt,
|
||||
selectAscStatement: selectAscStmt,
|
||||
}, nil
|
||||
deleteUserStmt: deleteUserStmt,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
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, &target, &account, &jsonMeta)
|
||||
err := result.Scan(&id, &operation, ×tamp, &initiator, &initiatorEmail, &target, &targetEmail, &account, &jsonMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -100,7 +137,17 @@ func processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
}
|
||||
}
|
||||
|
||||
events = append(events, &activity.Event{
|
||||
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),
|
||||
@ -108,7 +155,18 @@ func processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
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
|
||||
@ -127,13 +185,18 @@ func (store *Store) Get(accountID string, offset, limit int, descending bool) ([
|
||||
}
|
||||
|
||||
defer result.Close() //nolint
|
||||
return processResult(result)
|
||||
return store.processResult(result)
|
||||
}
|
||||
|
||||
// Save an event in the SQLite events table
|
||||
// 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
|
||||
if event.Meta != nil {
|
||||
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
|
||||
@ -156,6 +219,29 @@ func (store *Store) Save(event *activity.Event) (*activity.Event, error) {
|
||||
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 {
|
||||
|
@ -12,7 +12,8 @@ import (
|
||||
|
||||
func TestNewSQLiteStore(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
store, err := NewSQLiteStore(dataDir)
|
||||
key, _ := GenerateKey()
|
||||
store, err := NewSQLiteStore(dataDir, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
|
@ -35,7 +35,8 @@ type Config struct {
|
||||
TURNConfig *TURNConfig
|
||||
Signal *Host
|
||||
|
||||
Datadir string
|
||||
Datadir string
|
||||
DataStoreEncryptionKey string
|
||||
|
||||
HttpConfig *HttpServerConfig
|
||||
|
||||
|
@ -191,7 +191,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
return nil, err
|
||||
}
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.test", eventStore)
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.test", eventStore, false)
|
||||
}
|
||||
|
||||
func createDNSStore(t *testing.T) (Store, error) {
|
||||
|
0
management/server/http/api/generate.sh
Normal file → Executable file
0
management/server/http/api/generate.sh
Normal file → Executable file
@ -922,6 +922,10 @@ components:
|
||||
description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
|
||||
type: string
|
||||
example: google-oauth2|123456789012345678901
|
||||
initiator_email:
|
||||
description: The e-mail address of the initiator of the event. E.g., an e-mail of a user that triggered the event.
|
||||
type: string
|
||||
example: demo@netbird.io
|
||||
target_id:
|
||||
description: The ID of the target of the event. E.g., an ID of the peer that a user removed.
|
||||
type: string
|
||||
@ -938,6 +942,7 @@ components:
|
||||
- activity
|
||||
- activity_code
|
||||
- initiator_id
|
||||
- initiator_email
|
||||
- target_id
|
||||
- meta
|
||||
responses:
|
||||
|
@ -164,6 +164,9 @@ type Event struct {
|
||||
// Id Event unique identifier
|
||||
Id string `json:"id"`
|
||||
|
||||
// InitiatorEmail The e-mail address of the initiator of the event. E.g., an e-mail of a user that triggered the event.
|
||||
InitiatorEmail string `json:"initiator_email"`
|
||||
|
||||
// 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"`
|
||||
|
||||
|
@ -45,14 +45,46 @@ func (h *EventsHandler) GetAllEvents(w http.ResponseWriter, r *http.Request) {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
events := make([]*api.Event, 0)
|
||||
for _, e := range accountEvents {
|
||||
events = append(events, toEventResponse(e))
|
||||
events := make([]*api.Event, len(accountEvents))
|
||||
for i, e := range accountEvents {
|
||||
events[i] = toEventResponse(e)
|
||||
}
|
||||
|
||||
err = h.fillEventsWithInitiatorEmail(events, account.Id, user.Id)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, events)
|
||||
}
|
||||
|
||||
func (h *EventsHandler) fillEventsWithInitiatorEmail(events []*api.Event, accountId, userId string) error {
|
||||
// build email map based on users
|
||||
userInfos, err := h.accountManager.GetUsersFromAccount(accountId, userId)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get users from account: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
emails := make(map[string]string)
|
||||
for _, ui := range userInfos {
|
||||
emails[ui.ID] = ui.Email
|
||||
}
|
||||
|
||||
// fill event with email of initiator
|
||||
var ok bool
|
||||
for _, event := range events {
|
||||
if event.InitiatorEmail == "" {
|
||||
event.InitiatorEmail, ok = emails[event.InitiatorId]
|
||||
if !ok {
|
||||
log.Warnf("failed to resolve email for initiator: %s", event.InitiatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toEventResponse(event *activity.Event) *api.Event {
|
||||
meta := make(map[string]string)
|
||||
if event.Meta != nil {
|
||||
@ -60,13 +92,15 @@ func toEventResponse(event *activity.Event) *api.Event {
|
||||
meta[s] = fmt.Sprintf("%v", a)
|
||||
}
|
||||
}
|
||||
return &api.Event{
|
||||
Id: fmt.Sprint(event.ID),
|
||||
InitiatorId: event.InitiatorID,
|
||||
Activity: event.Activity.Message(),
|
||||
ActivityCode: api.EventActivityCode(event.Activity.StringCode()),
|
||||
TargetId: event.TargetID,
|
||||
Timestamp: event.Timestamp,
|
||||
Meta: meta,
|
||||
e := &api.Event{
|
||||
Id: fmt.Sprint(event.ID),
|
||||
InitiatorId: event.InitiatorID,
|
||||
InitiatorEmail: event.InitiatorEmail,
|
||||
Activity: event.Activity.Message(),
|
||||
ActivityCode: api.EventActivityCode(event.Activity.StringCode()),
|
||||
TargetId: event.TargetID,
|
||||
Timestamp: event.Timestamp,
|
||||
Meta: meta,
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
@ -37,6 +37,9 @@ func initEventsTestData(account string, user *server.User, events ...*activity.E
|
||||
},
|
||||
}, user, nil
|
||||
},
|
||||
GetUsersFromAccountFunc: func(accountID, userID string) ([]*server.UserInfo, error) {
|
||||
return make([]*server.UserInfo, 0), nil
|
||||
},
|
||||
},
|
||||
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
||||
jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
|
||||
|
@ -513,7 +513,9 @@ func buildUserExportRequest() (string, error) {
|
||||
return string(str), nil
|
||||
}
|
||||
|
||||
func (am *Auth0Manager) createPostRequest(endpoint string, payloadStr string) (*http.Request, error) {
|
||||
func (am *Auth0Manager) createRequest(
|
||||
method string, endpoint string, body io.Reader,
|
||||
) (*http.Request, error) {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -521,17 +523,23 @@ func (am *Auth0Manager) createPostRequest(endpoint string, payloadStr string) (*
|
||||
|
||||
reqURL := am.authIssuer + endpoint
|
||||
|
||||
payload := strings.NewReader(payloadStr)
|
||||
|
||||
req, err := http.NewRequest("POST", reqURL, payload)
|
||||
req, err := http.NewRequest(method, reqURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (am *Auth0Manager) createPostRequest(endpoint string, payloadStr string) (*http.Request, error) {
|
||||
req, err := am.createRequest("POST", endpoint, strings.NewReader(payloadStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
return req, nil
|
||||
|
||||
}
|
||||
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
@ -737,6 +745,38 @@ func (am *Auth0Manager) InviteUserByID(userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser from Auth0
|
||||
func (am *Auth0Manager) DeleteUser(userID string) error {
|
||||
req, err := am.createRequest(http.MethodDelete, "/api/v2/users/"+url.QueryEscape(userID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := am.httpClient.Do(req)
|
||||
if err != nil {
|
||||
log.Debugf("execute delete request: %v", err)
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
log.Errorf("close delete request body: %v", err)
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != 204 {
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkExportJobStatus checks the status of the job created at CreateExportUsersJob.
|
||||
// If the status is "completed", then return the downloadLink
|
||||
func (am *Auth0Manager) checkExportJobStatus(jobID string) (bool, string, error) {
|
||||
|
@ -12,9 +12,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api/v3"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
// AuthentikManager authentik manager client instance.
|
||||
@ -453,6 +454,38 @@ func (am *AuthentikManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from Authentik
|
||||
func (am *AuthentikManager) DeleteUser(userID string) error {
|
||||
ctx, err := am.authenticationContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userPk, err := strconv.ParseInt(userID, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := am.apiClient.CoreApi.CoreUsersDestroy(ctx, int32(userPk)).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close() // nolint
|
||||
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to delete user %s, statusCode %d", userID, resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AuthentikManager) authenticationContext() (context.Context, error) {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
|
@ -454,6 +454,43 @@ func (am *AzureManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from Azure
|
||||
func (am *AzureManager) DeleteUser(userID string) error {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/users/%s", am.GraphAPIEndpoint, url.QueryEscape(userID))
|
||||
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
log.Debugf("delete idp user %s", userID)
|
||||
|
||||
resp, err := am.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AzureManager) getUserExtensions() ([]azureExtension, error) {
|
||||
q := url.Values{}
|
||||
q.Add("$select", extensionFields)
|
||||
|
@ -254,6 +254,19 @@ func (gm *GoogleWorkspaceManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from GoogleWorkspace.
|
||||
func (gm *GoogleWorkspaceManager) DeleteUser(userID string) error {
|
||||
if err := gm.usersService.Delete(userID).Do(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gm.appMetrics != nil {
|
||||
gm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGoogleCredentials retrieves Google credentials based on the provided serviceAccountKey.
|
||||
// It decodes the base64-encoded serviceAccountKey and attempts to obtain credentials using it.
|
||||
// If that fails, it falls back to using the default Google credentials path.
|
||||
|
@ -18,6 +18,7 @@ type Manager interface {
|
||||
CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error)
|
||||
GetUserByEmail(email string) ([]*UserData, error)
|
||||
InviteUserByID(userID string) error
|
||||
DeleteUser(userID string) error
|
||||
}
|
||||
|
||||
// ClientConfig defines common client configuration for all IdP manager
|
||||
|
@ -467,6 +467,47 @@ func (km *KeycloakManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from Keycloack
|
||||
func (km *KeycloakManager) DeleteUser(userID string) error {
|
||||
jwtToken, err := km.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/users/%s", km.adminEndpoint, url.QueryEscape(userID))
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
resp, err := km.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close() // nolint
|
||||
|
||||
// In the docs, they specified 200, but in the endpoints, they return 204
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildKeycloakCreateUserRequestPayload(email string, name string, appMetadata AppMetadata) (string, error) {
|
||||
attrs := keycloakUserAttributes{}
|
||||
attrs.Set(wtAccountID, appMetadata.WTAccountID)
|
||||
|
@ -319,6 +319,28 @@ func (om *OktaManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from Okta
|
||||
func (om *OktaManager) DeleteUser(userID string) error {
|
||||
resp, err := om.client.User.DeactivateOrDeleteUser(context.Background(), userID, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if om.appMetrics != nil {
|
||||
om.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if om.appMetrics != nil {
|
||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateUserProfileSchema updates the Okta user schema to include custom fields,
|
||||
// wt_account_id and wt_pending_invite.
|
||||
func updateUserProfileSchema(client *okta.Client) error {
|
||||
|
@ -428,7 +428,7 @@ func (zm *ZitadelManager) UpdateUserAppMetadata(userID string, appMetadata AppMe
|
||||
return err
|
||||
}
|
||||
|
||||
resource := fmt.Sprintf("users/%s/metadata/_bulk", userID)
|
||||
resource := fmt.Sprintf("users/%s", userID)
|
||||
_, err = zm.post(resource, string(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
@ -447,6 +447,21 @@ func (zm *ZitadelManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from Zitadel
|
||||
func (zm *ZitadelManager) DeleteUser(userID string) error {
|
||||
resource := fmt.Sprintf("users/%s", userID)
|
||||
if err := zm.delete(resource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if zm.appMetrics != nil {
|
||||
zm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// getUserMetadata requests user metadata from zitadel via ID.
|
||||
func (zm *ZitadelManager) getUserMetadata(userID string) ([]zitadelMetadata, error) {
|
||||
resource := fmt.Sprintf("users/%s/metadata/_search", userID)
|
||||
@ -500,6 +515,42 @@ func (zm *ZitadelManager) post(resource string, body string) ([]byte, error) {
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// delete perform Delete requests.
|
||||
func (zm *ZitadelManager) delete(resource string) error {
|
||||
jwtToken, err := zm.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/%s", zm.managementEndpoint, resource)
|
||||
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
resp, err := zm.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if zm.appMetrics != nil {
|
||||
zm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if zm.appMetrics != nil {
|
||||
zm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to delete %s, statusCode %d", reqURL, resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// get perform Get requests.
|
||||
func (zm *ZitadelManager) get(resource string, q url.Values) ([]byte, error) {
|
||||
jwtToken, err := zm.credentials.Authenticate()
|
||||
|
@ -412,7 +412,7 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error)
|
||||
peersUpdateManager := NewPeersUpdateManager()
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "",
|
||||
eventStore)
|
||||
eventStore, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
@ -503,7 +503,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
|
||||
peersUpdateManager := server.NewPeersUpdateManager()
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||
eventStore)
|
||||
eventStore, false)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating a manager: %v", err)
|
||||
}
|
||||
|
@ -744,7 +744,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
return nil, err
|
||||
}
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore)
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore, false)
|
||||
}
|
||||
|
||||
func createNSStore(t *testing.T) (Store, error) {
|
||||
|
@ -681,7 +681,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
return nil, err
|
||||
}
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore)
|
||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore, false)
|
||||
}
|
||||
|
||||
func createRouterStore(t *testing.T) (Store, error) {
|
||||
|
@ -2,6 +2,7 @@ package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/metric/instrument"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
@ -13,6 +14,7 @@ type IDPMetrics struct {
|
||||
getUserByEmailCounter syncint64.Counter
|
||||
getAllAccountsCounter syncint64.Counter
|
||||
createUserCounter syncint64.Counter
|
||||
deleteUserCounter syncint64.Counter
|
||||
getAccountCounter syncint64.Counter
|
||||
getUserByIDCounter syncint64.Counter
|
||||
authenticateRequestCounter syncint64.Counter
|
||||
@ -39,6 +41,10 @@ func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deleteUserCounter, err := meter.SyncInt64().Counter("management.idp.delete.user.counter", instrument.WithUnit("1"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getAccountCounter, err := meter.SyncInt64().Counter("management.idp.get.account.counter", instrument.WithUnit("1"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -65,6 +71,7 @@ func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error)
|
||||
getUserByEmailCounter: getUserByEmailCounter,
|
||||
getAllAccountsCounter: getAllAccountsCounter,
|
||||
createUserCounter: createUserCounter,
|
||||
deleteUserCounter: deleteUserCounter,
|
||||
getAccountCounter: getAccountCounter,
|
||||
getUserByIDCounter: getUserByIDCounter,
|
||||
authenticateRequestCounter: authenticateRequestCounter,
|
||||
@ -88,6 +95,11 @@ func (idpMetrics *IDPMetrics) CountCreateUser() {
|
||||
idpMetrics.createUserCounter.Add(idpMetrics.ctx, 1)
|
||||
}
|
||||
|
||||
// CountDeleteUser ...
|
||||
func (idpMetrics *IDPMetrics) CountDeleteUser() {
|
||||
idpMetrics.deleteUserCounter.Add(idpMetrics.ctx, 1)
|
||||
}
|
||||
|
||||
// CountGetAllAccounts ...
|
||||
func (idpMetrics *IDPMetrics) CountGetAllAccounts() {
|
||||
idpMetrics.getAllAccountsCounter.Add(idpMetrics.ctx, 1)
|
||||
|
@ -327,15 +327,43 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t
|
||||
return status.Errorf(status.NotFound, "user not found")
|
||||
}
|
||||
if executingUser.Role != UserRoleAdmin {
|
||||
return status.Errorf(status.PermissionDenied, "only admins can delete service users")
|
||||
return status.Errorf(status.PermissionDenied, "only admins can delete users")
|
||||
}
|
||||
|
||||
if !targetUser.IsServiceUser {
|
||||
return status.Errorf(status.PermissionDenied, "regular users can not be deleted")
|
||||
peers, err := account.FindUserPeers(targetUserID)
|
||||
if err != nil {
|
||||
return status.Errorf(status.Internal, "failed to find user peers")
|
||||
}
|
||||
|
||||
meta := map[string]any{"name": targetUser.ServiceUserName}
|
||||
am.storeEvent(initiatorUserID, targetUserID, accountID, activity.ServiceUserDeleted, meta)
|
||||
if err := am.expireAndUpdatePeers(account, peers); err != nil {
|
||||
log.Errorf("failed update deleted peers expiration: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
targetUserEmail, err := am.getEmailOfTargetUser(account.Id, initiatorUserID, targetUserID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to resolve email address: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var meta map[string]any
|
||||
var eventAction activity.Activity
|
||||
if targetUser.IsServiceUser {
|
||||
meta = map[string]any{"name": targetUser.ServiceUserName}
|
||||
eventAction = activity.ServiceUserDeleted
|
||||
} else {
|
||||
meta = map[string]any{"email": targetUserEmail}
|
||||
eventAction = activity.UserDeleted
|
||||
|
||||
}
|
||||
am.storeEvent(initiatorUserID, targetUserID, accountID, eventAction, meta)
|
||||
|
||||
if !isNil(am.idpManager) {
|
||||
err := am.deleteUserFromIDP(targetUserID, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
delete(account.Users, targetUserID)
|
||||
|
||||
@ -609,23 +637,10 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var peerIDs []string
|
||||
for _, peer := range blockedPeers {
|
||||
peerIDs = append(peerIDs, peer.ID)
|
||||
peer.MarkLoginExpired(true)
|
||||
account.UpdatePeer(peer)
|
||||
err = am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status)
|
||||
if err != nil {
|
||||
log.Errorf("failed saving peer status while expiring peer %s", peer.ID)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
am.peersUpdateManager.CloseChannels(peerIDs)
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
log.Errorf("failed updating account peers while expiring peers of a blocked user %s", accountID)
|
||||
return nil, err
|
||||
|
||||
if err := am.expireAndUpdatePeers(account, blockedPeers); err != nil {
|
||||
log.Errorf("failed update expired peers: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -814,6 +829,67 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
|
||||
return userInfos, nil
|
||||
}
|
||||
|
||||
// expireAndUpdatePeers expires all peers of the given user and updates them in the account
|
||||
func (am *DefaultAccountManager) expireAndUpdatePeers(account *Account, peers []*Peer) error {
|
||||
var peerIDs []string
|
||||
for _, peer := range peers {
|
||||
peerIDs = append(peerIDs, peer.ID)
|
||||
peer.MarkLoginExpired(true)
|
||||
account.UpdatePeer(peer)
|
||||
if err := am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
am.storeEvent(
|
||||
peer.UserID, peer.ID, account.Id,
|
||||
activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()),
|
||||
)
|
||||
}
|
||||
|
||||
if len(peerIDs) != 0 {
|
||||
// this will trigger peer disconnect from the management service
|
||||
am.peersUpdateManager.CloseChannels(peerIDs)
|
||||
if err := am.updateAccountPeers(account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) deleteUserFromIDP(targetUserID, accountID string) error {
|
||||
if am.userDeleteFromIDPEnabled {
|
||||
log.Debugf("user %s deleted from IdP", targetUserID)
|
||||
err := am.idpManager.DeleteUser(targetUserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete user %s from IdP: %s", targetUserID, err)
|
||||
}
|
||||
} else {
|
||||
err := am.idpManager.UpdateUserAppMetadata(targetUserID, idp.AppMetadata{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove user %s app metadata in IdP: %s", targetUserID, err)
|
||||
}
|
||||
|
||||
_, err = am.refreshCache(accountID)
|
||||
if err != nil {
|
||||
log.Errorf("refresh account (%q) cache: %v", accountID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) getEmailOfTargetUser(accountId string, initiatorId, targetId string) (string, error) {
|
||||
userInfos, err := am.GetUsersFromAccount(accountId, initiatorId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, ui := range userInfos {
|
||||
if ui.ID == targetId {
|
||||
return ui.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("email not found for user: %s", targetId)
|
||||
}
|
||||
|
||||
func findUserInIDPUserdata(userID string, userData []*idp.UserData) (*idp.UserData, bool) {
|
||||
for _, user := range userData {
|
||||
if user.ID == userID {
|
||||
|
@ -439,8 +439,9 @@ func TestUser_DeleteUser_regularUser(t *testing.T) {
|
||||
}
|
||||
|
||||
err = am.DeleteUser(mockAccountID, mockUserID, mockUserID)
|
||||
|
||||
assert.Errorf(t, err, "Regular users can not be deleted (yet)")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_GetUser(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user