mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-22 00:11:29 +02:00
Compare commits
12 Commits
v0.23.6
...
fix/events
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56d82a99e1 | ||
|
|
d25f543913 | ||
|
|
672b4c0401 | ||
|
|
1219006a6e | ||
|
|
4791e41004 | ||
|
|
9131069d12 | ||
|
|
26bbc33e7a | ||
|
|
35bc493cc3 | ||
|
|
e26ec0b937 | ||
|
|
a952e7c72f | ||
|
|
22f69d7852 | ||
|
|
b23011fbe8 |
21
.github/workflows/test-infrastructure-files.yml
vendored
21
.github/workflows/test-infrastructure-files.yml
vendored
@@ -112,6 +112,27 @@ jobs:
|
||||
grep -A 6 PKCEAuthorizationFlow management.json | grep -A 5 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
|
||||
grep -A 7 PKCEAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
||||
|
||||
- name: Install modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: Build management binary
|
||||
working-directory: management
|
||||
run: CGO_ENABLED=1 go build -o netbird-mgmt main.go
|
||||
|
||||
- name: Build management docker image
|
||||
working-directory: management
|
||||
run: |
|
||||
docker build -t netbirdio/management:latest .
|
||||
|
||||
- name: Build signal binary
|
||||
working-directory: signal
|
||||
run: CGO_ENABLED=0 go build -o netbird-signal main.go
|
||||
|
||||
- name: Build signal docker image
|
||||
working-directory: signal
|
||||
run: |
|
||||
docker build -t netbirdio/signal:latest .
|
||||
|
||||
- name: run docker compose up
|
||||
working-directory: infrastructure_files
|
||||
run: |
|
||||
|
||||
@@ -197,7 +197,13 @@ func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo,
|
||||
tokenInfo.IDToken = idToken
|
||||
}
|
||||
|
||||
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), p.providerConfig.Audience); err != nil {
|
||||
// if a provider doesn't support an audience, use the Client ID for token verification
|
||||
audience := p.providerConfig.Audience
|
||||
if audience == "" {
|
||||
audience = p.providerConfig.ClientID
|
||||
}
|
||||
|
||||
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), audience); err != nil {
|
||||
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -106,9 +106,6 @@ func GetPKCEAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL
|
||||
|
||||
func isPKCEProviderConfigValid(config PKCEAuthProviderConfig) error {
|
||||
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
|
||||
if config.Audience == "" {
|
||||
return fmt.Errorf(errorMSGFormat, "Audience")
|
||||
}
|
||||
if config.ClientID == "" {
|
||||
return fmt.Errorf(errorMSGFormat, "Client ID")
|
||||
}
|
||||
|
||||
1
go.mod
1
go.mod
@@ -29,6 +29,7 @@ require (
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.1.4
|
||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible
|
||||
github.com/c-robinson/iplib v1.0.3
|
||||
github.com/cilium/ebpf v0.10.0
|
||||
github.com/coreos/go-iptables v0.7.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -61,6 +61,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo=
|
||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I=
|
||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 h1:pami0oPhVosjOu/qRHepRmdjD6hGILF7DBr+qQZeP10=
|
||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2/go.mod h1:jNIx5ykW1MroBuaTja9+VpglmaJOUzezumfhLlER3oY=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
|
||||
@@ -46,6 +46,14 @@ NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
# PKCE authorization flow
|
||||
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"}
|
||||
NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false}
|
||||
NETBIRD_AUTH_PKCE_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
||||
|
||||
# Dashboard
|
||||
|
||||
# The default setting is to transmit the audience to the IDP during authorization. However,
|
||||
# if your IDP does not have this capability, you can turn this off by setting it to false.
|
||||
NETBIRD_DASH_AUTH_USE_AUDIENCE=${NETBIRD_DASH_AUTH_USE_AUDIENCE:-true}
|
||||
NETBIRD_DASH_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
||||
|
||||
# exports
|
||||
export NETBIRD_DOMAIN
|
||||
@@ -86,4 +94,7 @@ export NETBIRD_TOKEN_SOURCE
|
||||
export NETBIRD_AUTH_DEVICE_AUTH_SCOPE
|
||||
export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
|
||||
export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
|
||||
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN
|
||||
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN
|
||||
export NETBIRD_AUTH_PKCE_AUDIENCE
|
||||
export NETBIRD_DASH_AUTH_USE_AUDIENCE
|
||||
export NETBIRD_DASH_AUTH_AUDIENCE
|
||||
@@ -164,6 +164,16 @@ done
|
||||
|
||||
export NETBIRD_AUTH_PKCE_REDIRECT_URLS=${REDIRECT_URLS%,}
|
||||
|
||||
# Remove audience for providers that do not support it
|
||||
if [ "$NETBIRD_DASH_AUTH_USE_AUDIENCE" = "false" ]; then
|
||||
export NETBIRD_DASH_AUTH_AUDIENCE=none
|
||||
export NETBIRD_AUTH_PKCE_AUDIENCE=
|
||||
fi
|
||||
|
||||
# Read the encryption key
|
||||
encKey=$(grep DataStoreEncryptionKey management.json | awk -F'"' '{$0=$4}1')
|
||||
export NETBIRD_DATASTORE_ENC_KEY=$encKey
|
||||
|
||||
env | grep NETBIRD
|
||||
|
||||
envsubst <docker-compose.yml.tmpl >docker-compose.yml
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||
# OIDC
|
||||
- AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
||||
- AUTH_AUDIENCE=$NETBIRD_DASH_AUTH_AUDIENCE
|
||||
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
||||
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
|
||||
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||
# OIDC
|
||||
- AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
||||
- AUTH_AUDIENCE=$NETBIRD_DASH_AUTH_AUDIENCE
|
||||
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
||||
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
|
||||
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"Password": null
|
||||
},
|
||||
"Datadir": "",
|
||||
"DataStoreEncryptionKey": "$NETBIRD_DATASTORE_ENC_KEY",
|
||||
"HttpConfig": {
|
||||
"Address": "0.0.0.0:$NETBIRD_MGMT_API_PORT",
|
||||
"AuthIssuer": "$NETBIRD_AUTH_AUTHORITY",
|
||||
@@ -62,7 +63,7 @@
|
||||
},
|
||||
"PKCEAuthorizationFlow": {
|
||||
"ProviderConfig": {
|
||||
"Audience": "$NETBIRD_AUTH_AUDIENCE",
|
||||
"Audience": "$NETBIRD_AUTH_PKCE_AUDIENCE",
|
||||
"ClientID": "$NETBIRD_AUTH_CLIENT_ID",
|
||||
"ClientSecret": "$NETBIRD_AUTH_CLIENT_SECRET",
|
||||
"AuthorizationEndpoint": "$NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT",
|
||||
|
||||
@@ -8,6 +8,9 @@ NETBIRD_DOMAIN=""
|
||||
# e.g., https://example.eu.auth0.com/.well-known/openid-configuration
|
||||
# -------------------------------------------
|
||||
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT=""
|
||||
# The default setting is to transmit the audience to the IDP during authorization. However,
|
||||
# if your IDP does not have this capability, you can turn this off by setting it to false.
|
||||
#NETBIRD_DASH_AUTH_USE_AUDIENCE=false
|
||||
NETBIRD_AUTH_AUDIENCE=""
|
||||
# e.g. netbird-client
|
||||
NETBIRD_AUTH_CLIENT_ID=""
|
||||
|
||||
@@ -301,15 +301,23 @@ 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()
|
||||
log.Debugf("restore or generate new activity store encryption key")
|
||||
key, err = sqlite.RestoreKey(dataDir)
|
||||
if err == nil {
|
||||
goto CreateStore
|
||||
} else {
|
||||
log.Debugf("failed to restore encryption key for activity store: %s", err)
|
||||
}
|
||||
|
||||
log.Infof("generate new encryption key for activity store")
|
||||
key, err = sqlite.GenerateKey(dataDir)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
CreateStore:
|
||||
store, err := sqlite.NewSQLiteStore(dataDir, key)
|
||||
return store, key, err
|
||||
|
||||
}
|
||||
|
||||
func notifyStop(msg string) {
|
||||
|
||||
@@ -62,12 +62,9 @@ type AccountManager interface {
|
||||
GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error)
|
||||
MarkPATUsed(tokenID string) error
|
||||
GetUser(claims jwtclaims.AuthorizationClaims) (*User, error)
|
||||
AccountExists(accountId string) (*bool, error)
|
||||
GetPeerByKey(peerKey string) (*Peer, error)
|
||||
GetPeers(accountID, userID string) ([]*Peer, error)
|
||||
MarkPeerConnected(peerKey string, connected bool) error
|
||||
DeletePeer(accountID, peerID, userID string) (*Peer, error)
|
||||
GetPeerByIP(accountId string, peerIP string) (*Peer, error)
|
||||
DeletePeer(accountID, peerID, userID string) error
|
||||
UpdatePeer(accountID, userID string, peer *Peer) (*Peer, error)
|
||||
GetNetworkMap(peerID string) (*NetworkMap, error)
|
||||
GetPeerNetwork(peerID string) (*Network, error)
|
||||
@@ -84,7 +81,6 @@ type AccountManager interface {
|
||||
ListGroups(accountId string) ([]*Group, error)
|
||||
GroupAddPeer(accountId, groupID, peerID string) error
|
||||
GroupDeletePeer(accountId, groupID, peerID string) error
|
||||
GroupListPeers(accountId, groupID string) ([]*Peer, error)
|
||||
GetPolicy(accountID, policyID, userID string) (*Policy, error)
|
||||
SavePolicy(accountID, userID string, policy *Policy) error
|
||||
DeletePolicy(accountID, policyID, userID string) error
|
||||
@@ -303,17 +299,6 @@ func (a *Account) GetRoutesByPrefix(prefix netip.Prefix) []*route.Route {
|
||||
return routes
|
||||
}
|
||||
|
||||
// GetPeerByIP returns peer by it's IP if exists under account or nil otherwise
|
||||
func (a *Account) GetPeerByIP(peerIP string) *Peer {
|
||||
for _, peer := range a.Peers {
|
||||
if peerIP == peer.IP.String() {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGroup returns a group by ID if exists, nil otherwise
|
||||
func (a *Account) GetGroup(groupID string) *Group {
|
||||
return a.Groups[groupID]
|
||||
@@ -637,8 +622,8 @@ func (a *Account) Copy() *Account {
|
||||
}
|
||||
|
||||
routes := map[string]*route.Route{}
|
||||
for id, route := range a.Routes {
|
||||
routes[id] = route.Copy()
|
||||
for id, r := range a.Routes {
|
||||
routes[id] = r.Copy()
|
||||
}
|
||||
|
||||
nsGroups := map[string]*nbdns.NameServerGroup{}
|
||||
@@ -988,6 +973,27 @@ func (am *DefaultAccountManager) warmupIDPCache() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the Identity Provider does not support writing AppMetadata,
|
||||
// in cases like this, we expect it to return all users in an "unset" field.
|
||||
// We iterate over the users in the "unset" field, look up their AccountID in our store, and
|
||||
// update their AppMetadata with the AccountID.
|
||||
if unsetData, ok := userData[idp.UnsetAccountID]; ok {
|
||||
for _, user := range unsetData {
|
||||
accountID, err := am.Store.GetAccountByUser(user.ID)
|
||||
if err == nil {
|
||||
data := userData[accountID.Id]
|
||||
if data == nil {
|
||||
data = make([]*idp.UserData, 0, 1)
|
||||
}
|
||||
|
||||
user.AppMetadata.WTAccountID = accountID.Id
|
||||
|
||||
userData[accountID.Id] = append(data, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(userData, idp.UnsetAccountID)
|
||||
|
||||
for accountID, users := range userData {
|
||||
err = am.cacheManager.Set(am.ctx, accountID, users, cacheStore.WithExpiration(cacheEntryExpiration()))
|
||||
if err != nil {
|
||||
@@ -1054,7 +1060,36 @@ func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(userID string, account
|
||||
|
||||
func (am *DefaultAccountManager) loadAccount(_ context.Context, accountID interface{}) ([]*idp.UserData, error) {
|
||||
log.Debugf("account %s not found in cache, reloading", accountID)
|
||||
return am.idpManager.GetAccount(fmt.Sprintf("%v", accountID))
|
||||
accountIDString := fmt.Sprintf("%v", accountID)
|
||||
|
||||
account, err := am.Store.GetAccount(accountIDString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userData, err := am.idpManager.GetAccount(accountIDString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataMap := make(map[string]*idp.UserData, len(userData))
|
||||
for _, datum := range userData {
|
||||
dataMap[datum.ID] = datum
|
||||
}
|
||||
|
||||
matchedUserData := make([]*idp.UserData, 0)
|
||||
for _, user := range account.Users {
|
||||
if user.IsServiceUser {
|
||||
continue
|
||||
}
|
||||
datum, ok := dataMap[user.Id]
|
||||
if !ok {
|
||||
log.Warnf("user %s not found in IDP", user.Id)
|
||||
continue
|
||||
}
|
||||
matchedUserData = append(matchedUserData, datum)
|
||||
}
|
||||
return matchedUserData, nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) lookupUserInCacheByEmail(email string, accountID string) (*idp.UserData, error) {
|
||||
@@ -1441,9 +1476,7 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat
|
||||
if err := am.Store.SaveAccount(account); err != nil {
|
||||
log.Errorf("failed to save account: %v", err)
|
||||
} else {
|
||||
if err := am.updateAccountPeers(account); err != nil {
|
||||
log.Errorf("failed updating account peers while updating user %s", account.Id)
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
for _, g := range addNewGroups {
|
||||
if group := account.GetGroup(g); group != nil {
|
||||
am.storeEvent(user.Id, user.Id, account.Id, activity.GroupAddedToUser,
|
||||
@@ -1554,26 +1587,6 @@ func isDomainValid(domain string) bool {
|
||||
return re.Match([]byte(domain))
|
||||
}
|
||||
|
||||
// AccountExists checks whether account exists (returns true) or not (returns false)
|
||||
func (am *DefaultAccountManager) AccountExists(accountID string) (*bool, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
var res bool
|
||||
_, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Type() == status.NotFound {
|
||||
res = false
|
||||
return &res, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
res = true
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// GetDNSDomain returns the configured dnsDomain
|
||||
func (am *DefaultAccountManager) GetDNSDomain() string {
|
||||
return am.dnsDomain
|
||||
|
||||
@@ -706,30 +706,6 @@ func createAccount(am *DefaultAccountManager, accountID, userID, domain string)
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func TestAccountManager_AccountExists(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
expectedId := "test_account"
|
||||
userId := "account_creator"
|
||||
_, err = createAccount(manager, expectedId, userId, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exists, err := manager.AccountExists(expectedId)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !*exists {
|
||||
t.Errorf("expected account to exist after creation, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountManager_GetAccount(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
@@ -1062,7 +1038,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := manager.DeletePeer(account.Id, peer3.ID, userID); err != nil {
|
||||
if err := manager.DeletePeer(account.Id, peer3.ID, userID); err != nil {
|
||||
t.Errorf("delete peer: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -1129,7 +1105,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = manager.DeletePeer(account.Id, peerKey, userID)
|
||||
err = manager.DeletePeer(account.Id, peerKey, userID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
backupFile = ".datastore.key"
|
||||
)
|
||||
|
||||
var iv = []byte{10, 22, 13, 79, 05, 8, 52, 91, 87, 98, 88, 98, 35, 25, 13, 05}
|
||||
@@ -15,16 +21,40 @@ type FieldEncrypt struct {
|
||||
block cipher.Block
|
||||
}
|
||||
|
||||
func GenerateKey() (string, error) {
|
||||
func RestoreKey(dataDir string) (string, error) {
|
||||
fName := filepath.Join(dataDir, backupFile)
|
||||
data, err := os.ReadFile(fName)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func GenerateKey(dataDir string) (string, error) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
readableKey := base64.StdEncoding.EncodeToString(key)
|
||||
|
||||
err = saveKey(dataDir, readableKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return readableKey, nil
|
||||
}
|
||||
|
||||
func saveKey(dataDir, key string) error {
|
||||
f, err := os.Create(filepath.Join(dataDir, backupFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFieldEncrypt(key string) (*FieldEncrypt, error) {
|
||||
binKey, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,11 +2,20 @@ package sqlite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestRestoreKey(t *testing.T) {
|
||||
_, err := RestoreKey(t.TempDir())
|
||||
if err != nil {
|
||||
log.Infof("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateKey(t *testing.T) {
|
||||
testData := "exampl@netbird.io"
|
||||
key, err := GenerateKey()
|
||||
key, err := GenerateKey(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %s", err)
|
||||
}
|
||||
@@ -32,7 +41,7 @@ func TestGenerateKey(t *testing.T) {
|
||||
|
||||
func TestCorruptKey(t *testing.T) {
|
||||
testData := "exampl@netbird.io"
|
||||
key, err := GenerateKey()
|
||||
key, err := GenerateKey(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %s", err)
|
||||
}
|
||||
@@ -46,7 +55,7 @@ func TestCorruptKey(t *testing.T) {
|
||||
t.Fatalf("invalid encrypted text")
|
||||
}
|
||||
|
||||
newKey, err := GenerateKey()
|
||||
newKey, err := GenerateKey(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %s", err)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ const (
|
||||
"VALUES(?, ?, ?, ?, ?, ?)"
|
||||
|
||||
insertDeleteUserQuery = `INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)`
|
||||
|
||||
fallbackName = "unknown"
|
||||
fallbackEmail = "unknown@unknown.com"
|
||||
)
|
||||
|
||||
// Store is the implementation of the activity.Store interface backed by SQLite
|
||||
@@ -128,6 +131,7 @@ func NewSQLiteStore(dataDir string, encryptionKey string) (*Store, error) {
|
||||
|
||||
func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
events := make([]*activity.Event, 0)
|
||||
var cryptErr error
|
||||
for result.Next() {
|
||||
var id int64
|
||||
var operation activity.Activity
|
||||
@@ -156,8 +160,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
if targetUserName != nil {
|
||||
name, err := store.fieldEncrypt.Decrypt(*targetUserName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt username for target id: %s", target)
|
||||
meta["username"] = ""
|
||||
cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", target)
|
||||
meta["username"] = fallbackName
|
||||
} else {
|
||||
meta["username"] = name
|
||||
}
|
||||
@@ -166,8 +170,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
if targetEmail != nil {
|
||||
email, err := store.fieldEncrypt.Decrypt(*targetEmail)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt email address for target id: %s", target)
|
||||
meta["email"] = ""
|
||||
cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", target)
|
||||
meta["email"] = fallbackEmail
|
||||
} else {
|
||||
meta["email"] = email
|
||||
}
|
||||
@@ -186,7 +190,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
if initiatorName != nil {
|
||||
name, err := store.fieldEncrypt.Decrypt(*initiatorName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt username of initiator: %s", initiator)
|
||||
cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", initiator)
|
||||
event.InitiatorName = fallbackName
|
||||
} else {
|
||||
event.InitiatorName = name
|
||||
}
|
||||
@@ -195,7 +200,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
if initiatorEmail != nil {
|
||||
email, err := store.fieldEncrypt.Decrypt(*initiatorEmail)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt email address of initiator: %s", initiator)
|
||||
cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", initiator)
|
||||
event.InitiatorEmail = fallbackEmail
|
||||
} else {
|
||||
event.InitiatorEmail = email
|
||||
}
|
||||
@@ -204,6 +210,10 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
if cryptErr != nil {
|
||||
log.Warnf("%s", cryptErr)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
func TestNewSQLiteStore(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
key, _ := GenerateKey()
|
||||
key, _ := GenerateKey(dataDir)
|
||||
store, err := NewSQLiteStore(dataDir, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -122,7 +122,9 @@ func (am *DefaultAccountManager) SaveDNSSettings(accountID string, userID string
|
||||
am.storeEvent(userID, accountID, accountID, activity.GroupRemovedFromDisabledManagementGroups, meta)
|
||||
}
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toProtocolDNSConfig(update nbdns.Config) *proto.DNSConfig {
|
||||
|
||||
@@ -162,7 +162,7 @@ func (e *EphemeralManager) cleanup() {
|
||||
|
||||
for id, p := range deletePeers {
|
||||
log.Debugf("delete ephemeral peer: %s", id)
|
||||
_, err := e.accountManager.DeletePeer(p.account.Id, id, activity.SystemInitiator)
|
||||
err := e.accountManager.DeletePeer(p.account.Id, id, activity.SystemInitiator)
|
||||
if err != nil {
|
||||
log.Tracef("failed to delete ephemeral peer: %s", err)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ type MocAccountManager struct {
|
||||
store *MockStore
|
||||
}
|
||||
|
||||
func (a MocAccountManager) DeletePeer(accountID, peerID, userID string) (*Peer, error) {
|
||||
func (a MocAccountManager) DeletePeer(accountID, peerID, userID string) error {
|
||||
delete(a.store.account.Peers, peerID)
|
||||
return nil, nil //nolint:nilnil
|
||||
return nil //nolint:nil
|
||||
}
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
|
||||
@@ -84,10 +84,7 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G
|
||||
return err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
// the following snippet tracks the activity and stores the group events in the event store.
|
||||
// It has to happen after all the operations have been successfully performed.
|
||||
@@ -229,7 +226,9 @@ func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string)
|
||||
|
||||
am.storeEvent(userId, groupID, accountId, activity.GroupDeleted, g.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListGroups objects of the peers
|
||||
@@ -281,7 +280,9 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string)
|
||||
return err
|
||||
}
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupDeletePeer removes peer from the group
|
||||
@@ -309,31 +310,7 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID stri
|
||||
}
|
||||
}
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
}
|
||||
|
||||
// GroupListPeers returns list of the peers from the group
|
||||
func (am *DefaultAccountManager) GroupListPeers(accountID, groupID string) ([]*Peer, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(status.NotFound, "account not found")
|
||||
}
|
||||
|
||||
group, ok := account.Groups[groupID]
|
||||
if !ok {
|
||||
return nil, status.Errorf(status.NotFound, "group with ID %s not found", groupID)
|
||||
}
|
||||
|
||||
peers := make([]*Peer, 0, len(account.Groups))
|
||||
for _, peerID := range group.Peers {
|
||||
p, ok := account.Peers[peerID]
|
||||
if ok {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,14 +53,6 @@ func initGroupTestData(user *server.User, groups ...*server.Group) *GroupsHandle
|
||||
Issued: server.GroupIssuedAPI,
|
||||
}, nil
|
||||
},
|
||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
||||
for _, peer := range TestPeers {
|
||||
if peer.IP.String() == peerIP {
|
||||
return peer, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("peer not found")
|
||||
},
|
||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
||||
return &server.Account{
|
||||
Id: claims.AccountId,
|
||||
|
||||
@@ -61,7 +61,7 @@ func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, pe
|
||||
}
|
||||
|
||||
func (h *PeersHandler) deletePeer(accountID, userID string, peerID string, w http.ResponseWriter) {
|
||||
_, err := h.accountManager.DeletePeer(accountID, peerID, userID)
|
||||
err := h.accountManager.DeletePeer(accountID, peerID, userID)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
|
||||
@@ -125,15 +125,6 @@ func initRoutesTestData() *RoutesHandler {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
||||
if peerIP != existingPeerID {
|
||||
return nil, status.Errorf(status.NotFound, "Peer with ID %s not found", peerIP)
|
||||
}
|
||||
return &server.Peer{
|
||||
Key: existingPeerKey,
|
||||
IP: netip.MustParseAddr(existingPeerID).AsSlice(),
|
||||
}, nil
|
||||
},
|
||||
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
||||
return testingAccount, testingAccount.Users["test_user"], nil
|
||||
},
|
||||
|
||||
@@ -210,47 +210,7 @@ func (ac *AuthentikCredentials) Authenticate() (JWTToken, error) {
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (am *AuthentikManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
||||
ctx, err := am.authenticationContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userPk, err := strconv.ParseInt(userID, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pendingInvite bool
|
||||
if appMetadata.WTPendingInvite != nil {
|
||||
pendingInvite = *appMetadata.WTPendingInvite
|
||||
}
|
||||
|
||||
patchedUserReq := api.PatchedUserRequest{
|
||||
Attributes: map[string]interface{}{
|
||||
wtAccountID: appMetadata.WTAccountID,
|
||||
wtPendingInvite: pendingInvite,
|
||||
},
|
||||
}
|
||||
_, resp, err := am.apiClient.CoreApi.CoreUsersPartialUpdate(ctx, int32(userPk)).
|
||||
PatchedUserRequest(patchedUserReq).
|
||||
Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to update user %s, statusCode %d", userID, resp.StatusCode)
|
||||
}
|
||||
|
||||
func (am *AuthentikManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -283,7 +243,10 @@ func (am *AuthentikManager) GetUserDataByID(userID string, appMetadata AppMetada
|
||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
||||
}
|
||||
|
||||
return parseAuthentikUser(*user)
|
||||
userData := parseAuthentikUser(*user)
|
||||
userData.AppMetadata = appMetadata
|
||||
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
@@ -293,8 +256,7 @@ func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountFilter := fmt.Sprintf("{%q:%q}", wtAccountID, accountID)
|
||||
userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Attributes(accountFilter).Execute()
|
||||
userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -313,10 +275,9 @@ func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, user := range userList.Results {
|
||||
userData, err := parseAuthentikUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userData := parseAuthentikUser(user)
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
users = append(users, userData)
|
||||
}
|
||||
|
||||
@@ -350,65 +311,16 @@ func (am *AuthentikManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, user := range userList.Results {
|
||||
userData, err := parseAuthentikUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountID := userData.AppMetadata.WTAccountID
|
||||
if accountID != "" {
|
||||
if _, ok := indexedUsers[accountID]; !ok {
|
||||
indexedUsers[accountID] = make([]*UserData, 0)
|
||||
}
|
||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
||||
}
|
||||
userData := parseAuthentikUser(user)
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in authentik Idp and sends an invitation.
|
||||
func (am *AuthentikManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
ctx, err := am.authenticationContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupID, err := am.getUserGroupByName("netbird")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultBoolValue := true
|
||||
createUserRequest := api.UserRequest{
|
||||
Email: &email,
|
||||
Name: name,
|
||||
IsActive: &defaultBoolValue,
|
||||
Groups: []string{groupID},
|
||||
Username: email,
|
||||
Attributes: map[string]interface{}{
|
||||
wtAccountID: accountID,
|
||||
wtPendingInvite: &defaultBoolValue,
|
||||
},
|
||||
}
|
||||
user, resp, err := am.apiClient.CoreApi.CoreUsersCreate(ctx).UserRequest(createUserRequest).Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return parseAuthentikUser(*user)
|
||||
func (am *AuthentikManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
@@ -438,11 +350,7 @@ func (am *AuthentikManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, user := range userList.Results {
|
||||
userData, err := parseAuthentikUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, userData)
|
||||
users = append(users, parseAuthentikUser(user))
|
||||
}
|
||||
|
||||
return users, nil
|
||||
@@ -501,64 +409,10 @@ func (am *AuthentikManager) authenticationContext() (context.Context, error) {
|
||||
return context.WithValue(context.Background(), api.ContextAPIKeys, value), nil
|
||||
}
|
||||
|
||||
// getUserGroupByName retrieves the user group for assigning new users.
|
||||
// If the group is not found, a new group with the specified name will be created.
|
||||
func (am *AuthentikManager) getUserGroupByName(name string) (string, error) {
|
||||
ctx, err := am.authenticationContext()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
groupList, resp, err := am.apiClient.CoreApi.CoreGroupsList(ctx).Name(name).Execute()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if groupList != nil {
|
||||
if len(groupList.Results) > 0 {
|
||||
return groupList.Results[0].Pk, nil
|
||||
}
|
||||
}
|
||||
|
||||
createGroupRequest := api.GroupRequest{Name: name}
|
||||
group, resp, err := am.apiClient.CoreApi.CoreGroupsCreate(ctx).GroupRequest(createGroupRequest).Execute()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return "", fmt.Errorf("unable to create user group, statusCode: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return group.Pk, nil
|
||||
}
|
||||
|
||||
func parseAuthentikUser(user api.User) (*UserData, error) {
|
||||
var attributes struct {
|
||||
AccountID string `json:"wt_account_id"`
|
||||
PendingInvite bool `json:"wt_pending_invite"`
|
||||
}
|
||||
|
||||
helper := JsonParser{}
|
||||
buf, err := helper.Marshal(user.Attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = helper.Unmarshal(buf, &attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func parseAuthentikUser(user api.User) *UserData {
|
||||
return &UserData{
|
||||
Email: *user.Email,
|
||||
Name: user.Name,
|
||||
ID: strconv.FormatInt(int64(user.Pk), 10),
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: attributes.AccountID,
|
||||
WTPendingInvite: &attributes.PendingInvite,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,18 +10,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
// azure extension properties template
|
||||
wtAccountIDTpl = "extension_%s_wt_account_id"
|
||||
wtPendingInviteTpl = "extension_%s_wt_pending_invite"
|
||||
|
||||
profileFields = "id,displayName,mail,userPrincipalName"
|
||||
extensionFields = "id,name,targetObjects"
|
||||
)
|
||||
const profileFields = "id,displayName,mail,userPrincipalName"
|
||||
|
||||
// AzureManager azure manager client instance.
|
||||
type AzureManager struct {
|
||||
@@ -58,21 +51,6 @@ type AzureCredentials struct {
|
||||
// azureProfile represents an azure user profile.
|
||||
type azureProfile map[string]any
|
||||
|
||||
// passwordProfile represent authentication method for,
|
||||
// newly created user profile.
|
||||
type passwordProfile struct {
|
||||
ForceChangePasswordNextSignIn bool `json:"forceChangePasswordNextSignIn"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// azureExtension represent custom attribute,
|
||||
// that can be added to user objects in Azure Active Directory (AD).
|
||||
type azureExtension struct {
|
||||
Name string `json:"name"`
|
||||
DataType string `json:"dataType"`
|
||||
TargetObjects []string `json:"targetObjects"`
|
||||
}
|
||||
|
||||
// NewAzureManager creates a new instance of the AzureManager.
|
||||
func NewAzureManager(config AzureClientConfig, appMetrics telemetry.AppMetrics) (*AzureManager, error) {
|
||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
@@ -115,7 +93,7 @@ func NewAzureManager(config AzureClientConfig, appMetrics telemetry.AppMetrics)
|
||||
appMetrics: appMetrics,
|
||||
}
|
||||
|
||||
manager := &AzureManager{
|
||||
return &AzureManager{
|
||||
ObjectID: config.ObjectID,
|
||||
ClientID: config.ClientID,
|
||||
GraphAPIEndpoint: config.GraphAPIEndpoint,
|
||||
@@ -123,14 +101,7 @@ func NewAzureManager(config AzureClientConfig, appMetrics telemetry.AppMetrics)
|
||||
credentials: credentials,
|
||||
helper: helper,
|
||||
appMetrics: appMetrics,
|
||||
}
|
||||
|
||||
err := manager.configureAppMetadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// jwtStillValid returns true if the token still valid and have enough time to be used and get a response from azure.
|
||||
@@ -236,44 +207,14 @@ func (ac *AzureCredentials) Authenticate() (JWTToken, error) {
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in azure AD Idp.
|
||||
func (am *AzureManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
payload, err := buildAzureCreateUserRequestPayload(email, name, accountID, am.ClientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := am.post("users", payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
var profile azureProfile
|
||||
err = am.helper.Unmarshal(body, &profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
||||
profile[wtAccountIDField] = accountID
|
||||
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
||||
profile[wtPendingInviteField] = true
|
||||
|
||||
return profile.userData(am.ClientID), nil
|
||||
func (am *AzureManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from keycloak via ID.
|
||||
func (am *AzureManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("$select", selectFields)
|
||||
q.Add("$select", profileFields)
|
||||
|
||||
body, err := am.get("users/"+userID, q)
|
||||
if err != nil {
|
||||
@@ -290,18 +231,17 @@ func (am *AzureManager) GetUserDataByID(userID string, appMetadata AppMetadata)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return profile.userData(am.ClientID), nil
|
||||
userData := profile.userData()
|
||||
userData.AppMetadata = appMetadata
|
||||
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
// If no users have been found, this function returns an empty list.
|
||||
func (am *AzureManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("$select", selectFields)
|
||||
q.Add("$select", profileFields)
|
||||
|
||||
body, err := am.get("users/"+email, q)
|
||||
if err != nil {
|
||||
@@ -319,20 +259,15 @@ func (am *AzureManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
users = append(users, profile.userData(am.ClientID))
|
||||
users = append(users, profile.userData())
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("$select", selectFields)
|
||||
q.Add("$filter", fmt.Sprintf("%s eq '%s'", wtAccountIDField, accountID))
|
||||
q.Add("$select", profileFields)
|
||||
|
||||
body, err := am.get("users", q)
|
||||
if err != nil {
|
||||
@@ -351,7 +286,10 @@ func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, profile := range profiles.Value {
|
||||
users = append(users, profile.userData(am.ClientID))
|
||||
userData := profile.userData()
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
users = append(users, userData)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
@@ -360,12 +298,8 @@ func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
// It returns a list of users indexed by accountID.
|
||||
func (am *AzureManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("$select", selectFields)
|
||||
q.Add("$select", profileFields)
|
||||
|
||||
body, err := am.get("users", q)
|
||||
if err != nil {
|
||||
@@ -384,67 +318,15 @@ func (am *AzureManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, profile := range profiles.Value {
|
||||
userData := profile.userData(am.ClientID)
|
||||
|
||||
accountID := userData.AppMetadata.WTAccountID
|
||||
if accountID != "" {
|
||||
if _, ok := indexedUsers[accountID]; !ok {
|
||||
indexedUsers[accountID] = make([]*UserData, 0)
|
||||
}
|
||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
||||
}
|
||||
|
||||
userData := profile.userData()
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID.
|
||||
func (am *AzureManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
||||
|
||||
data, err := am.helper.Marshal(map[string]any{
|
||||
wtAccountIDField: appMetadata.WTAccountID,
|
||||
wtPendingInviteField: appMetadata.WTPendingInvite,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := strings.NewReader(string(data))
|
||||
|
||||
reqURL := fmt.Sprintf("%s/users/%s", am.GraphAPIEndpoint, userID)
|
||||
req, err := http.NewRequest(http.MethodPatch, reqURL, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
log.Debugf("updating idp metadata for 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().CountUpdateUserAppMetadata()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("unable to update the appMetadata, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
func (am *AzureManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -454,7 +336,7 @@ func (am *AzureManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from Azure
|
||||
// DeleteUser from Azure.
|
||||
func (am *AzureManager) DeleteUser(userID string) error {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
@@ -491,81 +373,6 @@ func (am *AzureManager) DeleteUser(userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AzureManager) getUserExtensions() ([]azureExtension, error) {
|
||||
q := url.Values{}
|
||||
q.Add("$select", extensionFields)
|
||||
|
||||
resource := fmt.Sprintf("applications/%s/extensionProperties", am.ObjectID)
|
||||
body, err := am.get(resource, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var extensions struct{ Value []azureExtension }
|
||||
err = am.helper.Unmarshal(body, &extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extensions.Value, nil
|
||||
}
|
||||
|
||||
func (am *AzureManager) createUserExtension(name string) (*azureExtension, error) {
|
||||
extension := azureExtension{
|
||||
Name: name,
|
||||
DataType: "string",
|
||||
TargetObjects: []string{"User"},
|
||||
}
|
||||
|
||||
payload, err := am.helper.Marshal(extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resource := fmt.Sprintf("applications/%s/extensionProperties", am.ObjectID)
|
||||
body, err := am.post(resource, string(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userExtension azureExtension
|
||||
err = am.helper.Unmarshal(body, &userExtension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userExtension, nil
|
||||
}
|
||||
|
||||
// configureAppMetadata sets up app metadata extensions if they do not exists.
|
||||
func (am *AzureManager) configureAppMetadata() error {
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
||||
|
||||
extensions, err := am.getUserExtensions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the wt_account_id extension does not already exist, create it.
|
||||
if !hasExtension(extensions, wtAccountIDField) {
|
||||
_, err = am.createUserExtension(wtAccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If the wt_pending_invite extension does not already exist, create it.
|
||||
if !hasExtension(extensions, wtPendingInviteField) {
|
||||
_, err = am.createUserExtension(wtPendingInvite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// get perform Get requests.
|
||||
func (am *AzureManager) get(resource string, q url.Values) ([]byte, error) {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
@@ -602,44 +409,8 @@ func (am *AzureManager) get(resource string, q url.Values) ([]byte, error) {
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// post perform Post requests.
|
||||
func (am *AzureManager) post(resource string, body string) ([]byte, error) {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/%s", am.GraphAPIEndpoint, resource)
|
||||
req, err := http.NewRequest(http.MethodPost, reqURL, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
resp, err := am.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
if am.appMetrics != nil {
|
||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to post %s, statusCode %d", reqURL, resp.StatusCode)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// userData construct user data from keycloak profile.
|
||||
func (ap azureProfile) userData(clientID string) *UserData {
|
||||
func (ap azureProfile) userData() *UserData {
|
||||
id, ok := ap["id"].(string)
|
||||
if !ok {
|
||||
id = ""
|
||||
@@ -655,66 +426,9 @@ func (ap azureProfile) userData(clientID string) *UserData {
|
||||
name = ""
|
||||
}
|
||||
|
||||
accountIDField := extensionName(wtAccountIDTpl, clientID)
|
||||
accountID, ok := ap[accountIDField].(string)
|
||||
if !ok {
|
||||
accountID = ""
|
||||
}
|
||||
|
||||
pendingInviteField := extensionName(wtPendingInviteTpl, clientID)
|
||||
pendingInvite, ok := ap[pendingInviteField].(bool)
|
||||
if !ok {
|
||||
pendingInvite = false
|
||||
}
|
||||
|
||||
return &UserData{
|
||||
Email: email,
|
||||
Name: name,
|
||||
ID: id,
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
WTPendingInvite: &pendingInvite,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildAzureCreateUserRequestPayload(email, name, accountID, clientID string) (string, error) {
|
||||
wtAccountIDField := extensionName(wtAccountIDTpl, clientID)
|
||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, clientID)
|
||||
|
||||
req := &azureProfile{
|
||||
"accountEnabled": true,
|
||||
"displayName": name,
|
||||
"mailNickName": strings.Join(strings.Split(name, " "), ""),
|
||||
"userPrincipalName": email,
|
||||
"passwordProfile": passwordProfile{
|
||||
ForceChangePasswordNextSignIn: true,
|
||||
Password: GeneratePassword(8, 1, 1, 1),
|
||||
},
|
||||
wtAccountIDField: accountID,
|
||||
wtPendingInviteField: true,
|
||||
}
|
||||
|
||||
str, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(str), nil
|
||||
}
|
||||
|
||||
func extensionName(extensionTpl, clientID string) string {
|
||||
clientID = strings.ReplaceAll(clientID, "-", "")
|
||||
return fmt.Sprintf(extensionTpl, clientID)
|
||||
}
|
||||
|
||||
// hasExtension checks whether a given extension by name,
|
||||
// exists in an list of extensions.
|
||||
func hasExtension(extensions []azureExtension, name string) bool {
|
||||
for _, ext := range extensions {
|
||||
if ext.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -8,15 +8,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockAzureCredentials struct {
|
||||
jwtToken JWTToken
|
||||
err error
|
||||
}
|
||||
|
||||
func (mc *mockAzureCredentials) Authenticate() (JWTToken, error) {
|
||||
return mc.jwtToken, mc.err
|
||||
}
|
||||
|
||||
func TestAzureJwtStillValid(t *testing.T) {
|
||||
type jwtStillValidTest struct {
|
||||
name string
|
||||
@@ -124,206 +115,63 @@ func TestAzureAuthenticate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureUpdateUserAppMetadata(t *testing.T) {
|
||||
type updateUserAppMetadataTest struct {
|
||||
name string
|
||||
inputReqBody string
|
||||
expectedReqBody string
|
||||
appMetadata AppMetadata
|
||||
statusCode int
|
||||
helper ManagerHelper
|
||||
managerCreds ManagerCredentials
|
||||
assertErrFunc assert.ErrorAssertionFunc
|
||||
assertErrFuncMessage string
|
||||
}
|
||||
|
||||
appMetadata := AppMetadata{WTAccountID: "ok"}
|
||||
|
||||
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
||||
name: "Bad Authentication",
|
||||
expectedReqBody: "",
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 400,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockAzureCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
err: fmt.Errorf("error"),
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
||||
name: "Bad Status Code",
|
||||
expectedReqBody: fmt.Sprintf("{\"extension__wt_account_id\":\"%s\",\"extension__wt_pending_invite\":null}", appMetadata.WTAccountID),
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 400,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockAzureCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase3 := updateUserAppMetadataTest{
|
||||
name: "Bad Response Parsing",
|
||||
statusCode: 400,
|
||||
helper: &mockJsonParser{marshalErrorString: "error"},
|
||||
managerCreds: &mockAzureCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
||||
name: "Good request",
|
||||
expectedReqBody: fmt.Sprintf("{\"extension__wt_account_id\":\"%s\",\"extension__wt_pending_invite\":null}", appMetadata.WTAccountID),
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 204,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockAzureCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
invite := true
|
||||
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
|
||||
name: "Update Pending Invite",
|
||||
expectedReqBody: fmt.Sprintf("{\"extension__wt_account_id\":\"%s\",\"extension__wt_pending_invite\":true}", appMetadata.WTAccountID),
|
||||
appMetadata: AppMetadata{
|
||||
WTAccountID: "ok",
|
||||
WTPendingInvite: &invite,
|
||||
},
|
||||
statusCode: 204,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockAzureCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2,
|
||||
updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4, updateUserAppMetadataTestCase5} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
reqClient := mockHTTPClient{
|
||||
resBody: testCase.inputReqBody,
|
||||
code: testCase.statusCode,
|
||||
}
|
||||
|
||||
manager := &AzureManager{
|
||||
httpClient: &reqClient,
|
||||
credentials: testCase.managerCreds,
|
||||
helper: testCase.helper,
|
||||
}
|
||||
|
||||
err := manager.UpdateUserAppMetadata("1", testCase.appMetadata)
|
||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
||||
|
||||
assert.Equal(t, testCase.expectedReqBody, reqClient.reqBody, "request body should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProfile(t *testing.T) {
|
||||
type azureProfileTest struct {
|
||||
name string
|
||||
clientID string
|
||||
invite bool
|
||||
inputProfile azureProfile
|
||||
expectedUserData UserData
|
||||
}
|
||||
|
||||
azureProfileTestCase1 := azureProfileTest{
|
||||
name: "Good Request",
|
||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
||||
invite: false,
|
||||
name: "Good Request",
|
||||
invite: false,
|
||||
inputProfile: azureProfile{
|
||||
"id": "test1",
|
||||
"displayName": "John Doe",
|
||||
"userPrincipalName": "test1@test.com",
|
||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_account_id": "1",
|
||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_pending_invite": false,
|
||||
},
|
||||
expectedUserData: UserData{
|
||||
Email: "test1@test.com",
|
||||
Name: "John Doe",
|
||||
ID: "test1",
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
azureProfileTestCase2 := azureProfileTest{
|
||||
name: "Missing User ID",
|
||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
||||
invite: true,
|
||||
name: "Missing User ID",
|
||||
invite: true,
|
||||
inputProfile: azureProfile{
|
||||
"displayName": "John Doe",
|
||||
"userPrincipalName": "test2@test.com",
|
||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_account_id": "1",
|
||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_pending_invite": true,
|
||||
},
|
||||
expectedUserData: UserData{
|
||||
Email: "test2@test.com",
|
||||
Name: "John Doe",
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
azureProfileTestCase3 := azureProfileTest{
|
||||
name: "Missing User Name",
|
||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
||||
invite: false,
|
||||
name: "Missing User Name",
|
||||
invite: false,
|
||||
inputProfile: azureProfile{
|
||||
"id": "test3",
|
||||
"userPrincipalName": "test3@test.com",
|
||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_account_id": "1",
|
||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_pending_invite": false,
|
||||
},
|
||||
expectedUserData: UserData{
|
||||
ID: "test3",
|
||||
Email: "test3@test.com",
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
azureProfileTestCase4 := azureProfileTest{
|
||||
name: "Missing Extension Fields",
|
||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
||||
invite: false,
|
||||
inputProfile: azureProfile{
|
||||
"id": "test4",
|
||||
"displayName": "John Doe",
|
||||
"userPrincipalName": "test4@test.com",
|
||||
},
|
||||
expectedUserData: UserData{
|
||||
ID: "test4",
|
||||
Name: "John Doe",
|
||||
Email: "test4@test.com",
|
||||
AppMetadata: AppMetadata{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range []azureProfileTest{azureProfileTestCase1, azureProfileTestCase2, azureProfileTestCase3, azureProfileTestCase4} {
|
||||
for _, testCase := range []azureProfileTest{azureProfileTestCase1, azureProfileTestCase2, azureProfileTestCase3} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
testCase.expectedUserData.AppMetadata.WTPendingInvite = &testCase.invite
|
||||
userData := testCase.inputProfile.userData(testCase.clientID)
|
||||
userData := testCase.inputProfile.userData()
|
||||
|
||||
assert.Equal(t, testCase.expectedUserData.ID, userData.ID, "User id should match")
|
||||
assert.Equal(t, testCase.expectedUserData.Email, userData.Email, "User email should match")
|
||||
assert.Equal(t, testCase.expectedUserData.Name, userData.Name, "User name should match")
|
||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTAccountID, userData.AppMetadata.WTAccountID, "Account id should match")
|
||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTPendingInvite, userData.AppMetadata.WTPendingInvite, "Pending invite should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,14 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2/google"
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
// GoogleWorkspaceManager Google Workspace manager client instance.
|
||||
@@ -73,17 +72,13 @@ func NewGoogleWorkspaceManager(config GoogleWorkspaceClientConfig, appMetrics te
|
||||
}
|
||||
|
||||
service, err := admin.NewService(context.Background(),
|
||||
option.WithScopes(admin.AdminDirectoryUserScope, admin.AdminDirectoryUserschemaScope),
|
||||
option.WithScopes(admin.AdminDirectoryUserReadonlyScope),
|
||||
option.WithCredentials(adminCredentials),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = configureAppMetadataSchema(service, config.CustomerID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GoogleWorkspaceManager{
|
||||
usersService: service.Users,
|
||||
CustomerID: config.CustomerID,
|
||||
@@ -95,27 +90,7 @@ func NewGoogleWorkspaceManager(config GoogleWorkspaceClientConfig, appMetrics te
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (gm *GoogleWorkspaceManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
||||
metadata, err := gm.helper.Marshal(appMetadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := &admin.User{
|
||||
CustomSchemas: map[string]googleapi.RawMessage{
|
||||
"app_metadata": metadata,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = gm.usersService.Update(userID, user).Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gm.appMetrics != nil {
|
||||
gm.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
||||
}
|
||||
|
||||
func (gm *GoogleWorkspaceManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -130,23 +105,23 @@ func (gm *GoogleWorkspaceManager) GetUserDataByID(userID string, appMetadata App
|
||||
gm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
||||
}
|
||||
|
||||
return parseGoogleWorkspaceUser(user)
|
||||
userData := parseGoogleWorkspaceUser(user)
|
||||
userData.AppMetadata = appMetadata
|
||||
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
func (gm *GoogleWorkspaceManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
query := fmt.Sprintf("app_metadata.wt_account_id=\"%s\"", accountID)
|
||||
usersList, err := gm.usersService.List().Customer(gm.CustomerID).Query(query).Projection("full").Do()
|
||||
usersList, err := gm.usersService.List().Customer(gm.CustomerID).Projection("full").Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usersData := make([]*UserData, 0)
|
||||
for _, user := range usersList.Users {
|
||||
userData, err := parseGoogleWorkspaceUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userData := parseGoogleWorkspaceUser(user)
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
usersData = append(usersData, userData)
|
||||
}
|
||||
@@ -168,61 +143,16 @@ func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, erro
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, user := range usersList.Users {
|
||||
userData, err := parseGoogleWorkspaceUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountID := userData.AppMetadata.WTAccountID
|
||||
if accountID != "" {
|
||||
if _, ok := indexedUsers[accountID]; !ok {
|
||||
indexedUsers[accountID] = make([]*UserData, 0)
|
||||
}
|
||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
||||
}
|
||||
userData := parseGoogleWorkspaceUser(user)
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in Google Workspace and sends an invitation.
|
||||
func (gm *GoogleWorkspaceManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
invite := true
|
||||
metadata := AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
WTPendingInvite: &invite,
|
||||
}
|
||||
|
||||
username := &admin.UserName{}
|
||||
fields := strings.Fields(name)
|
||||
if n := len(fields); n > 0 {
|
||||
username.GivenName = strings.Join(fields[:n-1], " ")
|
||||
username.FamilyName = fields[n-1]
|
||||
}
|
||||
|
||||
payload, err := gm.helper.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &admin.User{
|
||||
Name: username,
|
||||
PrimaryEmail: email,
|
||||
CustomSchemas: map[string]googleapi.RawMessage{
|
||||
"app_metadata": payload,
|
||||
},
|
||||
Password: GeneratePassword(8, 1, 1, 1),
|
||||
}
|
||||
user, err = gm.usersService.Insert(user).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gm.appMetrics != nil {
|
||||
gm.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
return parseGoogleWorkspaceUser(user)
|
||||
func (gm *GoogleWorkspaceManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
@@ -237,13 +167,8 @@ func (gm *GoogleWorkspaceManager) GetUserByEmail(email string) ([]*UserData, err
|
||||
gm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
||||
}
|
||||
|
||||
userData, err := parseGoogleWorkspaceUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
users = append(users, userData)
|
||||
users = append(users, parseGoogleWorkspaceUser(user))
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -281,8 +206,7 @@ func getGoogleCredentials(serviceAccountKey string) (*google.Credentials, error)
|
||||
creds, err := google.CredentialsFromJSON(
|
||||
context.Background(),
|
||||
decodeKey,
|
||||
admin.AdminDirectoryUserschemaScope,
|
||||
admin.AdminDirectoryUserScope,
|
||||
admin.AdminDirectoryUserReadonlyScope,
|
||||
)
|
||||
if err == nil {
|
||||
// No need to fallback to the default Google credentials path
|
||||
@@ -294,8 +218,7 @@ func getGoogleCredentials(serviceAccountKey string) (*google.Credentials, error)
|
||||
|
||||
creds, err = google.FindDefaultCredentials(
|
||||
context.Background(),
|
||||
admin.AdminDirectoryUserschemaScope,
|
||||
admin.AdminDirectoryUserScope,
|
||||
admin.AdminDirectoryUserReadonlyScope,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -304,62 +227,11 @@ func getGoogleCredentials(serviceAccountKey string) (*google.Credentials, error)
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// configureAppMetadataSchema create a custom schema for managing app metadata fields in Google Workspace.
|
||||
func configureAppMetadataSchema(service *admin.Service, customerID string) error {
|
||||
schemaList, err := service.Schemas.List(customerID).Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// checks if app_metadata schema is already created
|
||||
for _, schema := range schemaList.Schemas {
|
||||
if schema.SchemaName == "app_metadata" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// create new app_metadata schema
|
||||
appMetadataSchema := &admin.Schema{
|
||||
SchemaName: "app_metadata",
|
||||
Fields: []*admin.SchemaFieldSpec{
|
||||
{
|
||||
FieldName: "wt_account_id",
|
||||
FieldType: "STRING",
|
||||
MultiValued: false,
|
||||
},
|
||||
{
|
||||
FieldName: "wt_pending_invite",
|
||||
FieldType: "BOOL",
|
||||
MultiValued: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = service.Schemas.Insert(customerID, appMetadataSchema).Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseGoogleWorkspaceUser parse google user to UserData.
|
||||
func parseGoogleWorkspaceUser(user *admin.User) (*UserData, error) {
|
||||
var appMetadata AppMetadata
|
||||
|
||||
// Get app metadata from custom schemas
|
||||
if user.CustomSchemas != nil {
|
||||
rawMessage := user.CustomSchemas["app_metadata"]
|
||||
helper := JsonParser{}
|
||||
|
||||
if err := helper.Unmarshal(rawMessage, &appMetadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func parseGoogleWorkspaceUser(user *admin.User) *UserData {
|
||||
return &UserData{
|
||||
ID: user.Id,
|
||||
Email: user.PrimaryEmail,
|
||||
Name: user.Name.FullName,
|
||||
AppMetadata: appMetadata,
|
||||
}, nil
|
||||
ID: user.Id,
|
||||
Email: user.PrimaryEmail,
|
||||
Name: user.Name.FullName,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
// UnsetAccountID is a special key to map users without an account ID
|
||||
UnsetAccountID = "unset"
|
||||
)
|
||||
|
||||
// Manager idp manager interface
|
||||
type Manager interface {
|
||||
UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error
|
||||
@@ -171,7 +176,11 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
||||
CustomerID: config.ExtraConfig["CustomerId"],
|
||||
}
|
||||
return NewGoogleWorkspaceManager(googleClientConfig, appMetrics)
|
||||
|
||||
case "jumpcloud":
|
||||
jumpcloudConfig := JumpCloudClientConfig{
|
||||
APIToken: config.ExtraConfig["ApiToken"],
|
||||
}
|
||||
return NewJumpCloudManager(jumpcloudConfig, appMetrics)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
|
||||
}
|
||||
|
||||
257
management/server/idp/jumpcloud.go
Normal file
257
management/server/idp/jumpcloud.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/TheJumpCloud/jcapi-go/v1"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
accept = "application/json"
|
||||
)
|
||||
|
||||
// JumpCloudManager JumpCloud manager client instance.
|
||||
type JumpCloudManager struct {
|
||||
client *v1.APIClient
|
||||
apiToken string
|
||||
httpClient ManagerHTTPClient
|
||||
credentials ManagerCredentials
|
||||
helper ManagerHelper
|
||||
appMetrics telemetry.AppMetrics
|
||||
}
|
||||
|
||||
// JumpCloudClientConfig JumpCloud manager client configurations.
|
||||
type JumpCloudClientConfig struct {
|
||||
APIToken string
|
||||
}
|
||||
|
||||
// JumpCloudCredentials JumpCloud authentication information.
|
||||
type JumpCloudCredentials struct {
|
||||
clientConfig JumpCloudClientConfig
|
||||
helper ManagerHelper
|
||||
httpClient ManagerHTTPClient
|
||||
appMetrics telemetry.AppMetrics
|
||||
}
|
||||
|
||||
// NewJumpCloudManager creates a new instance of the JumpCloudManager.
|
||||
func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppMetrics) (*JumpCloudManager, error) {
|
||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
httpTransport.MaxIdleConns = 5
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: httpTransport,
|
||||
}
|
||||
helper := JsonParser{}
|
||||
|
||||
if config.APIToken == "" {
|
||||
return nil, fmt.Errorf("jumpCloud IdP configuration is incomplete, ApiToken is missing")
|
||||
}
|
||||
|
||||
client := v1.NewAPIClient(v1.NewConfiguration())
|
||||
credentials := &JumpCloudCredentials{
|
||||
clientConfig: config,
|
||||
httpClient: httpClient,
|
||||
helper: helper,
|
||||
appMetrics: appMetrics,
|
||||
}
|
||||
|
||||
return &JumpCloudManager{
|
||||
client: client,
|
||||
apiToken: config.APIToken,
|
||||
httpClient: httpClient,
|
||||
credentials: credentials,
|
||||
helper: helper,
|
||||
appMetrics: appMetrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Authenticate retrieves access token to use the JumpCloud user API.
|
||||
func (jc *JumpCloudCredentials) Authenticate() (JWTToken, error) {
|
||||
return JWTToken{}, nil
|
||||
}
|
||||
|
||||
func (jm *JumpCloudManager) authenticationContext() context.Context {
|
||||
return context.WithValue(context.Background(), v1.ContextAPIKey, v1.APIKey{
|
||||
Key: jm.apiToken,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (jm *JumpCloudManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from JumpCloud via ID.
|
||||
func (jm *JumpCloudManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||
authCtx := jm.authenticationContext()
|
||||
user, resp, err := jm.client.SystemusersApi.SystemusersGet(authCtx, userID, contentType, accept, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
||||
}
|
||||
|
||||
userData := parseJumpCloudUser(user)
|
||||
userData.AppMetadata = appMetadata
|
||||
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
func (jm *JumpCloudManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
authCtx := jm.authenticationContext()
|
||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get account %s users, statusCode %d", accountID, resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetAccount()
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, user := range userList.Results {
|
||||
userData := parseJumpCloudUser(user)
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
users = append(users, userData)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
// It returns a list of users indexed by accountID.
|
||||
func (jm *JumpCloudManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
authCtx := jm.authenticationContext()
|
||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||
}
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, user := range userList.Results {
|
||||
userData := parseJumpCloudUser(user)
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in JumpCloud Idp and sends an invitation.
|
||||
func (jm *JumpCloudManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
// If no users have been found, this function returns an empty list.
|
||||
func (jm *JumpCloudManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||
searchFilter := map[string]interface{}{
|
||||
"searchFilter": map[string]interface{}{
|
||||
"filter": []string{email},
|
||||
"fields": []string{"email"},
|
||||
},
|
||||
}
|
||||
|
||||
authCtx := jm.authenticationContext()
|
||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, searchFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", email, resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
||||
}
|
||||
|
||||
usersData := make([]*UserData, 0)
|
||||
for _, user := range userList.Results {
|
||||
usersData = append(usersData, parseJumpCloudUser(user))
|
||||
}
|
||||
|
||||
return usersData, nil
|
||||
}
|
||||
|
||||
// InviteUserByID resend invitations to users who haven't activated,
|
||||
// their accounts prior to the expiration period.
|
||||
func (jm *JumpCloudManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from jumpCloud directory
|
||||
func (jm *JumpCloudManager) DeleteUser(userID string) error {
|
||||
authCtx := jm.authenticationContext()
|
||||
_, resp, err := jm.client.SystemusersApi.SystemusersDelete(authCtx, userID, contentType, accept, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseJumpCloudUser parse JumpCloud system user returned from API V1 to UserData.
|
||||
func parseJumpCloudUser(user v1.Systemuserreturn) *UserData {
|
||||
names := []string{user.Firstname, user.Middlename, user.Lastname}
|
||||
return &UserData{
|
||||
Email: user.Email,
|
||||
Name: strings.Join(names, " "),
|
||||
ID: user.Id,
|
||||
}
|
||||
}
|
||||
46
management/server/idp/jumpcloud_test.go
Normal file
46
management/server/idp/jumpcloud_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
func TestNewJumpCloudManager(t *testing.T) {
|
||||
type test struct {
|
||||
name string
|
||||
inputConfig JumpCloudClientConfig
|
||||
assertErrFunc require.ErrorAssertionFunc
|
||||
assertErrFuncMessage string
|
||||
}
|
||||
|
||||
defaultTestConfig := JumpCloudClientConfig{
|
||||
APIToken: "test123",
|
||||
}
|
||||
|
||||
testCase1 := test{
|
||||
name: "Good Configuration",
|
||||
inputConfig: defaultTestConfig,
|
||||
assertErrFunc: require.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
testCase2Config := defaultTestConfig
|
||||
testCase2Config.APIToken = ""
|
||||
|
||||
testCase2 := test{
|
||||
name: "Missing APIToken Configuration",
|
||||
inputConfig: testCase2Config,
|
||||
assertErrFunc: require.Error,
|
||||
assertErrFuncMessage: "should return error when field empty",
|
||||
}
|
||||
|
||||
for _, testCase := range []test{testCase1, testCase2} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
_, err := NewJumpCloudManager(testCase.inputConfig, &telemetry.MockAppMetrics{})
|
||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,11 +16,6 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
wtAccountID = "wt_account_id"
|
||||
wtPendingInvite = "wt_pending_invite"
|
||||
)
|
||||
|
||||
// KeycloakManager keycloak manager client instance.
|
||||
type KeycloakManager struct {
|
||||
adminEndpoint string
|
||||
@@ -51,28 +44,10 @@ type KeycloakCredentials struct {
|
||||
appMetrics telemetry.AppMetrics
|
||||
}
|
||||
|
||||
// keycloakUserCredential describe the authentication method for,
|
||||
// newly created user profile.
|
||||
type keycloakUserCredential struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
Temporary bool `json:"temporary"`
|
||||
}
|
||||
|
||||
// keycloakUserAttributes holds additional user data fields.
|
||||
type keycloakUserAttributes map[string][]string
|
||||
|
||||
// createUserRequest is a user create request.
|
||||
type keycloakCreateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
Credentials []keycloakUserCredential `json:"credentials"`
|
||||
Attributes keycloakUserAttributes `json:"attributes"`
|
||||
}
|
||||
|
||||
// keycloakProfile represents an keycloak user profile response.
|
||||
// keycloakProfile represents a keycloak user profile response.
|
||||
type keycloakProfile struct {
|
||||
ID string `json:"id"`
|
||||
CreatedTimestamp int64 `json:"createdTimestamp"`
|
||||
@@ -230,62 +205,8 @@ func (kc *KeycloakCredentials) Authenticate() (JWTToken, error) {
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in keycloak Idp and sends an invite.
|
||||
func (km *KeycloakManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
jwtToken, err := km.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
invite := true
|
||||
appMetadata := AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
WTPendingInvite: &invite,
|
||||
}
|
||||
|
||||
payloadString, err := buildKeycloakCreateUserRequestPayload(email, name, appMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/users", km.adminEndpoint)
|
||||
payload := strings.NewReader(payloadString)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, reqURL, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
resp, err := km.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
locationHeader := resp.Header.Get("location")
|
||||
userID, err := extractUserIDFromLocationHeader(locationHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return km.GetUserDataByID(userID, appMetadata)
|
||||
func (km *KeycloakManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
@@ -319,7 +240,7 @@ func (km *KeycloakManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from keycloak via ID.
|
||||
func (km *KeycloakManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||
func (km *KeycloakManager) GetUserDataByID(userID string, _ AppMetadata) (*UserData, error) {
|
||||
body, err := km.get("users/"+userID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -338,12 +259,9 @@ func (km *KeycloakManager) GetUserDataByID(userID string, appMetadata AppMetadat
|
||||
return profile.userData(), nil
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
// GetAccount returns all the users for a given account profile.
|
||||
func (km *KeycloakManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
q := url.Values{}
|
||||
q.Add("q", wtAccountID+":"+accountID)
|
||||
|
||||
body, err := km.get("users", q)
|
||||
profiles, err := km.fetchAllUserProfiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -352,15 +270,12 @@ func (km *KeycloakManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
km.appMetrics.IDPMetrics().CountGetAccount()
|
||||
}
|
||||
|
||||
profiles := make([]keycloakProfile, 0)
|
||||
err = km.helper.Unmarshal(body, &profiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, profile := range profiles {
|
||||
users = append(users, profile.userData())
|
||||
userData := profile.userData()
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
users = append(users, userData)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
@@ -369,15 +284,7 @@ func (km *KeycloakManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
// It returns a list of users indexed by accountID.
|
||||
func (km *KeycloakManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
totalUsers, err := km.totalUsersCount()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("max", fmt.Sprint(*totalUsers))
|
||||
|
||||
body, err := km.get("users", q)
|
||||
profiles, err := km.fetchAllUserProfiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -386,78 +293,17 @@ func (km *KeycloakManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
km.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||
}
|
||||
|
||||
profiles := make([]keycloakProfile, 0)
|
||||
err = km.helper.Unmarshal(body, &profiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, profile := range profiles {
|
||||
userData := profile.userData()
|
||||
|
||||
accountID := userData.AppMetadata.WTAccountID
|
||||
if accountID != "" {
|
||||
if _, ok := indexedUsers[accountID]; !ok {
|
||||
indexedUsers[accountID] = make([]*UserData, 0)
|
||||
}
|
||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
||||
}
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (km *KeycloakManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
||||
jwtToken, err := km.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs := keycloakUserAttributes{}
|
||||
attrs.Set(wtAccountID, appMetadata.WTAccountID)
|
||||
if appMetadata.WTPendingInvite != nil {
|
||||
attrs.Set(wtPendingInvite, strconv.FormatBool(*appMetadata.WTPendingInvite))
|
||||
} else {
|
||||
attrs.Set(wtPendingInvite, "false")
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/users/%s", km.adminEndpoint, userID)
|
||||
data, err := km.helper.Marshal(map[string]any{
|
||||
"attributes": attrs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := strings.NewReader(string(data))
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, reqURL, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
log.Debugf("updating IdP metadata for user %s", userID)
|
||||
|
||||
resp, err := km.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if km.appMetrics != nil {
|
||||
km.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("unable to update the appMetadata, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
func (km *KeycloakManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -467,7 +313,7 @@ func (km *KeycloakManager) InviteUserByID(_ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser from Keycloack
|
||||
// DeleteUser from Keycloak by user ID.
|
||||
func (km *KeycloakManager) DeleteUser(userID string) error {
|
||||
jwtToken, err := km.credentials.Authenticate()
|
||||
if err != nil {
|
||||
@@ -475,7 +321,6 @@ func (km *KeycloakManager) DeleteUser(userID string) error {
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/users/%s", km.adminEndpoint, url.QueryEscape(userID))
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -508,32 +353,27 @@ func (km *KeycloakManager) DeleteUser(userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildKeycloakCreateUserRequestPayload(email string, name string, appMetadata AppMetadata) (string, error) {
|
||||
attrs := keycloakUserAttributes{}
|
||||
attrs.Set(wtAccountID, appMetadata.WTAccountID)
|
||||
attrs.Set(wtPendingInvite, strconv.FormatBool(*appMetadata.WTPendingInvite))
|
||||
|
||||
req := &keycloakCreateUserRequest{
|
||||
Email: email,
|
||||
Username: name,
|
||||
Enabled: true,
|
||||
EmailVerified: true,
|
||||
Credentials: []keycloakUserCredential{
|
||||
{
|
||||
Type: "password",
|
||||
Value: GeneratePassword(8, 1, 1, 1),
|
||||
Temporary: false,
|
||||
},
|
||||
},
|
||||
Attributes: attrs,
|
||||
}
|
||||
|
||||
str, err := json.Marshal(req)
|
||||
func (km *KeycloakManager) fetchAllUserProfiles() ([]keycloakProfile, error) {
|
||||
totalUsers, err := km.totalUsersCount()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return string(str), nil
|
||||
q := url.Values{}
|
||||
q.Add("max", fmt.Sprint(*totalUsers))
|
||||
|
||||
body, err := km.get("users", q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profiles := make([]keycloakProfile, 0)
|
||||
err = km.helper.Unmarshal(body, &profiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// get perform Get requests.
|
||||
@@ -588,53 +428,11 @@ func (km *KeycloakManager) totalUsersCount() (*int, error) {
|
||||
return &count, nil
|
||||
}
|
||||
|
||||
// extractUserIDFromLocationHeader extracts the user ID from the location,
|
||||
// header once the user is created successfully
|
||||
func extractUserIDFromLocationHeader(locationHeader string) (string, error) {
|
||||
userURL, err := url.Parse(locationHeader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Base(userURL.Path), nil
|
||||
}
|
||||
|
||||
// userData construct user data from keycloak profile.
|
||||
func (kp keycloakProfile) userData() *UserData {
|
||||
accountID := kp.Attributes.Get(wtAccountID)
|
||||
pendingInvite, err := strconv.ParseBool(kp.Attributes.Get(wtPendingInvite))
|
||||
if err != nil {
|
||||
pendingInvite = false
|
||||
}
|
||||
|
||||
return &UserData{
|
||||
Email: kp.Email,
|
||||
Name: kp.Username,
|
||||
ID: kp.ID,
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
WTPendingInvite: &pendingInvite,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the key to value. It replaces any existing
|
||||
// values.
|
||||
func (ka keycloakUserAttributes) Set(key, value string) {
|
||||
ka[key] = []string{value}
|
||||
}
|
||||
|
||||
// Get returns the first value associated with the given key.
|
||||
// If there are no values associated with the key, Get returns
|
||||
// the empty string.
|
||||
func (ka keycloakUserAttributes) Get(key string) string {
|
||||
if ka == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
values := ka[key]
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
@@ -84,15 +84,6 @@ func TestNewKeycloakManager(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type mockKeycloakCredentials struct {
|
||||
jwtToken JWTToken
|
||||
err error
|
||||
}
|
||||
|
||||
func (mc *mockKeycloakCredentials) Authenticate() (JWTToken, error) {
|
||||
return mc.jwtToken, mc.err
|
||||
}
|
||||
|
||||
func TestKeycloakRequestJWTToken(t *testing.T) {
|
||||
|
||||
type requestJWTTokenTest struct {
|
||||
@@ -316,108 +307,3 @@ func TestKeycloakAuthenticate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeycloakUpdateUserAppMetadata(t *testing.T) {
|
||||
type updateUserAppMetadataTest struct {
|
||||
name string
|
||||
inputReqBody string
|
||||
expectedReqBody string
|
||||
appMetadata AppMetadata
|
||||
statusCode int
|
||||
helper ManagerHelper
|
||||
managerCreds ManagerCredentials
|
||||
assertErrFunc assert.ErrorAssertionFunc
|
||||
assertErrFuncMessage string
|
||||
}
|
||||
|
||||
appMetadata := AppMetadata{WTAccountID: "ok"}
|
||||
|
||||
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
||||
name: "Bad Authentication",
|
||||
expectedReqBody: "",
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 400,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockKeycloakCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
err: fmt.Errorf("error"),
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
||||
name: "Bad Status Code",
|
||||
expectedReqBody: fmt.Sprintf("{\"attributes\":{\"wt_account_id\":[\"%s\"],\"wt_pending_invite\":[\"false\"]}}", appMetadata.WTAccountID),
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 400,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockKeycloakCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase3 := updateUserAppMetadataTest{
|
||||
name: "Bad Response Parsing",
|
||||
statusCode: 400,
|
||||
helper: &mockJsonParser{marshalErrorString: "error"},
|
||||
managerCreds: &mockKeycloakCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
||||
name: "Good request",
|
||||
expectedReqBody: fmt.Sprintf("{\"attributes\":{\"wt_account_id\":[\"%s\"],\"wt_pending_invite\":[\"false\"]}}", appMetadata.WTAccountID),
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 204,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockKeycloakCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
invite := true
|
||||
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
|
||||
name: "Update Pending Invite",
|
||||
expectedReqBody: fmt.Sprintf("{\"attributes\":{\"wt_account_id\":[\"%s\"],\"wt_pending_invite\":[\"true\"]}}", appMetadata.WTAccountID),
|
||||
appMetadata: AppMetadata{
|
||||
WTAccountID: "ok",
|
||||
WTPendingInvite: &invite,
|
||||
},
|
||||
statusCode: 204,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockKeycloakCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2,
|
||||
updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4, updateUserAppMetadataTestCase5} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
reqClient := mockHTTPClient{
|
||||
resBody: testCase.inputReqBody,
|
||||
code: testCase.statusCode,
|
||||
}
|
||||
|
||||
manager := &KeycloakManager{
|
||||
httpClient: &reqClient,
|
||||
credentials: testCase.managerCreds,
|
||||
helper: testCase.helper,
|
||||
}
|
||||
|
||||
err := manager.UpdateUserAppMetadata("1", testCase.appMetadata)
|
||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
||||
|
||||
assert.Equal(t, testCase.expectedReqBody, reqClient.reqBody, "request body should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"github.com/okta/okta-sdk-golang/v2/okta"
|
||||
"github.com/okta/okta-sdk-golang/v2/okta/query"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
// OktaManager okta manager client instance.
|
||||
@@ -76,11 +76,6 @@ func NewOktaManager(config OktaClientConfig, appMetrics telemetry.AppMetrics) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updateUserProfileSchema(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
credentials := &OktaCredentials{
|
||||
clientConfig: config,
|
||||
httpClient: httpClient,
|
||||
@@ -103,49 +98,8 @@ func (oc *OktaCredentials) Authenticate() (JWTToken, error) {
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in okta Idp and sends an invitation.
|
||||
func (om *OktaManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
var (
|
||||
sendEmail = true
|
||||
activate = true
|
||||
userProfile = okta.UserProfile{
|
||||
"email": email,
|
||||
"login": email,
|
||||
wtAccountID: accountID,
|
||||
wtPendingInvite: true,
|
||||
}
|
||||
)
|
||||
|
||||
fields := strings.Fields(name)
|
||||
if n := len(fields); n > 0 {
|
||||
userProfile["firstName"] = strings.Join(fields[:n-1], " ")
|
||||
userProfile["lastName"] = fields[n-1]
|
||||
}
|
||||
|
||||
user, resp, err := om.client.User.CreateUser(context.Background(),
|
||||
okta.CreateUserRequest{
|
||||
Profile: &userProfile,
|
||||
},
|
||||
&query.Params{
|
||||
Activate: &activate,
|
||||
SendEmail: &sendEmail,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if om.appMetrics != nil {
|
||||
om.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if om.appMetrics != nil {
|
||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return parseOktaUser(user)
|
||||
func (om *OktaManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from keycloak via ID.
|
||||
@@ -166,7 +120,13 @@ func (om *OktaManager) GetUserDataByID(userID string, appMetadata AppMetadata) (
|
||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
||||
}
|
||||
|
||||
return parseOktaUser(user)
|
||||
userData, err := parseOktaUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userData.AppMetadata = appMetadata
|
||||
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
@@ -200,8 +160,7 @@ func (om *OktaManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
search := fmt.Sprintf("profile.wt_account_id eq %q", accountID)
|
||||
users, resp, err := om.client.User.ListUsers(context.Background(), &query.Params{Search: search})
|
||||
users, resp, err := om.client.User.ListUsers(context.Background(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -223,6 +182,7 @@ func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
list = append(list, userData)
|
||||
}
|
||||
@@ -256,13 +216,7 @@ func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountID := userData.AppMetadata.WTAccountID
|
||||
if accountID != "" {
|
||||
if _, ok := indexedUsers[accountID]; !ok {
|
||||
indexedUsers[accountID] = make([]*UserData, 0)
|
||||
}
|
||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
||||
}
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
@@ -270,46 +224,6 @@ func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (om *OktaManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
||||
user, resp, err := om.client.User.GetUser(context.Background(), userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if om.appMetrics != nil {
|
||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to update user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
profile := *user.Profile
|
||||
|
||||
if appMetadata.WTPendingInvite != nil {
|
||||
profile[wtPendingInvite] = *appMetadata.WTPendingInvite
|
||||
}
|
||||
|
||||
if appMetadata.WTAccountID != "" {
|
||||
profile[wtAccountID] = appMetadata.WTAccountID
|
||||
}
|
||||
|
||||
user.Profile = &profile
|
||||
_, resp, err = om.client.User.UpdateUser(context.Background(), userID, *user, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if om.appMetrics != nil {
|
||||
om.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if om.appMetrics != nil {
|
||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to update user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -341,60 +255,12 @@ func (om *OktaManager) DeleteUser(userID string) error {
|
||||
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 {
|
||||
// Ensure Okta doesn't enforce user input for these fields, as they are solely used by Netbird
|
||||
userPermissions := []*okta.UserSchemaAttributePermission{{Action: "HIDE", Principal: "SELF"}}
|
||||
|
||||
_, resp, err := client.UserSchema.UpdateUserProfile(
|
||||
context.Background(),
|
||||
"default",
|
||||
okta.UserSchema{
|
||||
Definitions: &okta.UserSchemaDefinitions{
|
||||
Custom: &okta.UserSchemaPublic{
|
||||
Id: "#custom",
|
||||
Type: "object",
|
||||
Properties: map[string]*okta.UserSchemaAttribute{
|
||||
wtAccountID: {
|
||||
MaxLength: 100,
|
||||
MinLength: 1,
|
||||
Required: new(bool),
|
||||
Scope: "NONE",
|
||||
Title: "Wt Account Id",
|
||||
Type: "string",
|
||||
Permissions: userPermissions,
|
||||
},
|
||||
wtPendingInvite: {
|
||||
Required: new(bool),
|
||||
Scope: "NONE",
|
||||
Title: "Wt Pending Invite",
|
||||
Type: "boolean",
|
||||
Permissions: userPermissions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unable to update user profile schema, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseOktaUserToUserData parse okta user to UserData.
|
||||
func parseOktaUser(user *okta.User) (*UserData, error) {
|
||||
var oktaUser struct {
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
AccountID string `json:"wt_account_id"`
|
||||
PendingInvite bool `json:"wt_pending_invite"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
@@ -418,9 +284,5 @@ func parseOktaUser(user *okta.User) (*UserData, error) {
|
||||
Email: oktaUser.Email,
|
||||
Name: strings.Join([]string{oktaUser.FirstName, oktaUser.LastName}, " "),
|
||||
ID: user.Id,
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: oktaUser.AccountID,
|
||||
WTPendingInvite: &oktaUser.PendingInvite,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/okta/okta-sdk-golang/v2/okta"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseOktaUser(t *testing.T) {
|
||||
type parseOktaUserTest struct {
|
||||
name string
|
||||
invite bool
|
||||
inputProfile *okta.User
|
||||
expectedUserData *UserData
|
||||
assertErrFunc assert.ErrorAssertionFunc
|
||||
}
|
||||
|
||||
parseOktaTestCase1 := parseOktaUserTest{
|
||||
name: "Good Request",
|
||||
invite: true,
|
||||
name: "Good Request",
|
||||
inputProfile: &okta.User{
|
||||
Id: "123",
|
||||
Profile: &okta.UserProfile{
|
||||
"email": "test@example.com",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"wt_account_id": "456",
|
||||
"wt_pending_invite": true,
|
||||
"email": "test@example.com",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
},
|
||||
},
|
||||
expectedUserData: &UserData{
|
||||
@@ -41,36 +38,17 @@ func TestParseOktaUser(t *testing.T) {
|
||||
|
||||
parseOktaTestCase2 := parseOktaUserTest{
|
||||
name: "Invalid okta user",
|
||||
invite: true,
|
||||
inputProfile: nil,
|
||||
expectedUserData: nil,
|
||||
assertErrFunc: assert.Error,
|
||||
}
|
||||
|
||||
parseOktaTestCase3 := parseOktaUserTest{
|
||||
name: "Invalid pending invite type",
|
||||
invite: false,
|
||||
inputProfile: &okta.User{
|
||||
Id: "123",
|
||||
Profile: &okta.UserProfile{
|
||||
"email": "test@example.com",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"wt_account_id": "456",
|
||||
"wt_pending_invite": "true",
|
||||
},
|
||||
},
|
||||
expectedUserData: nil,
|
||||
assertErrFunc: assert.Error,
|
||||
}
|
||||
|
||||
for _, testCase := range []parseOktaUserTest{parseOktaTestCase1, parseOktaTestCase2, parseOktaTestCase3} {
|
||||
for _, testCase := range []parseOktaUserTest{parseOktaTestCase1, parseOktaTestCase2} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
userData, err := parseOktaUser(testCase.inputProfile)
|
||||
testCase.assertErrFunc(t, err, testCase.assertErrFunc)
|
||||
|
||||
if err == nil {
|
||||
testCase.expectedUserData.AppMetadata.WTPendingInvite = &testCase.invite
|
||||
assert.True(t, userDataEqual(testCase.expectedUserData, userData), "user data should match")
|
||||
}
|
||||
})
|
||||
@@ -83,13 +61,5 @@ func userDataEqual(a, b *UserData) bool {
|
||||
if a.Email != b.Email || a.Name != b.Name || a.ID != b.ID {
|
||||
return false
|
||||
}
|
||||
if a.AppMetadata.WTAccountID != b.AppMetadata.WTAccountID {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.AppMetadata.WTPendingInvite != nil && b.AppMetadata.WTPendingInvite != nil &&
|
||||
*a.AppMetadata.WTPendingInvite != *b.AppMetadata.WTPendingInvite {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -68,12 +65,6 @@ type zitadelUser struct {
|
||||
|
||||
type zitadelAttributes map[string][]map[string]any
|
||||
|
||||
// zitadelMetadata holds additional user data.
|
||||
type zitadelMetadata struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// zitadelProfile represents an zitadel user profile response.
|
||||
type zitadelProfile struct {
|
||||
ID string `json:"id"`
|
||||
@@ -82,7 +73,6 @@ type zitadelProfile struct {
|
||||
PreferredLoginName string `json:"preferredLoginName"`
|
||||
LoginNames []string `json:"loginNames"`
|
||||
Human *zitadelUser `json:"human"`
|
||||
Metadata []zitadelMetadata
|
||||
}
|
||||
|
||||
// NewZitadelManager creates a new instance of the ZitadelManager.
|
||||
@@ -235,42 +225,8 @@ func (zc *ZitadelCredentials) Authenticate() (JWTToken, error) {
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in zitadel Idp and sends an invite.
|
||||
func (zm *ZitadelManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
payload, err := buildZitadelCreateUserRequestPayload(email, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := zm.post("users/human/_import", payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if zm.appMetrics != nil {
|
||||
zm.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
var result struct {
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
err = zm.helper.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
invite := true
|
||||
appMetadata := AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
WTPendingInvite: &invite,
|
||||
}
|
||||
|
||||
// Add metadata to new user
|
||||
err = zm.UpdateUserAppMetadata(result.UserID, appMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return zm.GetUserDataByID(result.UserID, appMetadata)
|
||||
func (zm *ZitadelManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
@@ -308,12 +264,6 @@ func (zm *ZitadelManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, profile := range profiles.Result {
|
||||
metadata, err := zm.getUserMetadata(profile.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.Metadata = metadata
|
||||
|
||||
users = append(users, profile.userData())
|
||||
}
|
||||
|
||||
@@ -337,18 +287,15 @@ func (zm *ZitadelManager) GetUserDataByID(userID string, appMetadata AppMetadata
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata, err := zm.getUserMetadata(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.User.Metadata = metadata
|
||||
userData := profile.User.userData()
|
||||
userData.AppMetadata = appMetadata
|
||||
|
||||
return profile.User.userData(), nil
|
||||
return userData, nil
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
func (zm *ZitadelManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
accounts, err := zm.GetAllAccounts()
|
||||
body, err := zm.post("users/_search", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -357,7 +304,21 @@ func (zm *ZitadelManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||
zm.appMetrics.IDPMetrics().CountGetAccount()
|
||||
}
|
||||
|
||||
return accounts[accountID], nil
|
||||
var profiles struct{ Result []zitadelProfile }
|
||||
err = zm.helper.Unmarshal(body, &profiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, profile := range profiles.Result {
|
||||
userData := profile.userData()
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
users = append(users, userData)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
@@ -380,22 +341,8 @@ func (zm *ZitadelManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, profile := range profiles.Result {
|
||||
// fetch user metadata
|
||||
metadata, err := zm.getUserMetadata(profile.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.Metadata = metadata
|
||||
|
||||
userData := profile.userData()
|
||||
accountID := userData.AppMetadata.WTAccountID
|
||||
|
||||
if accountID != "" {
|
||||
if _, ok := indexedUsers[accountID]; !ok {
|
||||
indexedUsers[accountID] = make([]*UserData, 0)
|
||||
}
|
||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
||||
}
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
@@ -403,42 +350,7 @@ func (zm *ZitadelManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
// Metadata values are base64 encoded.
|
||||
func (zm *ZitadelManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
||||
if appMetadata.WTPendingInvite == nil {
|
||||
appMetadata.WTPendingInvite = new(bool)
|
||||
}
|
||||
pendingInviteBuf := strconv.AppendBool([]byte{}, *appMetadata.WTPendingInvite)
|
||||
|
||||
wtAccountIDValue := base64.StdEncoding.EncodeToString([]byte(appMetadata.WTAccountID))
|
||||
wtPendingInviteValue := base64.StdEncoding.EncodeToString(pendingInviteBuf)
|
||||
|
||||
metadata := zitadelAttributes{
|
||||
"metadata": {
|
||||
{
|
||||
"key": wtAccountID,
|
||||
"value": wtAccountIDValue,
|
||||
},
|
||||
{
|
||||
"key": wtPendingInvite,
|
||||
"value": wtPendingInviteValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
payload, err := zm.helper.Marshal(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resource := fmt.Sprintf("users/%s/metadata/_bulk", userID)
|
||||
_, err = zm.post(resource, string(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if zm.appMetrics != nil {
|
||||
zm.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
||||
}
|
||||
|
||||
func (zm *ZitadelManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -460,24 +372,6 @@ func (zm *ZitadelManager) DeleteUser(userID string) error {
|
||||
}
|
||||
|
||||
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)
|
||||
body, err := zm.post(resource, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var metadata struct{ Result []zitadelMetadata }
|
||||
err = zm.helper.Unmarshal(body, &metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return metadata.Result, nil
|
||||
}
|
||||
|
||||
// post perform Post requests.
|
||||
@@ -517,38 +411,7 @@ func (zm *ZitadelManager) post(resource string, body string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (zm *ZitadelManager) delete(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -588,38 +451,13 @@ func (zm *ZitadelManager) get(resource string, q url.Values) ([]byte, error) {
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// value returns string represented by the base64 string value.
|
||||
func (zm zitadelMetadata) value() string {
|
||||
value, err := base64.StdEncoding.DecodeString(zm.Value)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(value)
|
||||
}
|
||||
|
||||
// userData construct user data from zitadel profile.
|
||||
func (zp zitadelProfile) userData() *UserData {
|
||||
var (
|
||||
email string
|
||||
name string
|
||||
wtAccountIDValue string
|
||||
wtPendingInviteValue bool
|
||||
email string
|
||||
name string
|
||||
)
|
||||
|
||||
for _, metadata := range zp.Metadata {
|
||||
if metadata.Key == wtAccountID {
|
||||
wtAccountIDValue = metadata.value()
|
||||
}
|
||||
|
||||
if metadata.Key == wtPendingInvite {
|
||||
value, err := strconv.ParseBool(metadata.value())
|
||||
if err == nil {
|
||||
wtPendingInviteValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain the email for the human account and the login name,
|
||||
// for the machine account.
|
||||
if zp.Human != nil {
|
||||
@@ -636,39 +474,5 @@ func (zp zitadelProfile) userData() *UserData {
|
||||
Email: email,
|
||||
Name: name,
|
||||
ID: zp.ID,
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: wtAccountIDValue,
|
||||
WTPendingInvite: &wtPendingInviteValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildZitadelCreateUserRequestPayload(email string, name string) (string, error) {
|
||||
var firstName, lastName string
|
||||
|
||||
words := strings.Fields(name)
|
||||
if n := len(words); n > 0 {
|
||||
firstName = strings.Join(words[:n-1], " ")
|
||||
lastName = words[n-1]
|
||||
}
|
||||
|
||||
req := &zitadelUser{
|
||||
UserName: name,
|
||||
Profile: zitadelUserInfo{
|
||||
FirstName: strings.TrimSpace(firstName),
|
||||
LastName: strings.TrimSpace(lastName),
|
||||
DisplayName: name,
|
||||
},
|
||||
Email: zitadelEmail{
|
||||
Email: email,
|
||||
IsEmailVerified: false,
|
||||
},
|
||||
}
|
||||
|
||||
str, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(str), nil
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
func TestNewZitadelManager(t *testing.T) {
|
||||
@@ -63,15 +64,6 @@ func TestNewZitadelManager(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type mockZitadelCredentials struct {
|
||||
jwtToken JWTToken
|
||||
err error
|
||||
}
|
||||
|
||||
func (mc *mockZitadelCredentials) Authenticate() (JWTToken, error) {
|
||||
return mc.jwtToken, mc.err
|
||||
}
|
||||
|
||||
func TestZitadelRequestJWTToken(t *testing.T) {
|
||||
|
||||
type requestJWTTokenTest struct {
|
||||
@@ -296,98 +288,6 @@ func TestZitadelAuthenticate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestZitadelUpdateUserAppMetadata(t *testing.T) {
|
||||
type updateUserAppMetadataTest struct {
|
||||
name string
|
||||
inputReqBody string
|
||||
expectedReqBody string
|
||||
appMetadata AppMetadata
|
||||
statusCode int
|
||||
helper ManagerHelper
|
||||
managerCreds ManagerCredentials
|
||||
assertErrFunc assert.ErrorAssertionFunc
|
||||
assertErrFuncMessage string
|
||||
}
|
||||
|
||||
appMetadata := AppMetadata{WTAccountID: "ok"}
|
||||
|
||||
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
||||
name: "Bad Authentication",
|
||||
expectedReqBody: "",
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 400,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockZitadelCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
err: fmt.Errorf("error"),
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
||||
name: "Bad Response Parsing",
|
||||
statusCode: 400,
|
||||
helper: &mockJsonParser{marshalErrorString: "error"},
|
||||
managerCreds: &mockZitadelCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.Error,
|
||||
assertErrFuncMessage: "should return error",
|
||||
}
|
||||
|
||||
updateUserAppMetadataTestCase3 := updateUserAppMetadataTest{
|
||||
name: "Good request",
|
||||
expectedReqBody: "{\"metadata\":[{\"key\":\"wt_account_id\",\"value\":\"b2s=\"},{\"key\":\"wt_pending_invite\",\"value\":\"ZmFsc2U=\"}]}",
|
||||
appMetadata: appMetadata,
|
||||
statusCode: 200,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockZitadelCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
invite := true
|
||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
||||
name: "Update Pending Invite",
|
||||
expectedReqBody: "{\"metadata\":[{\"key\":\"wt_account_id\",\"value\":\"b2s=\"},{\"key\":\"wt_pending_invite\",\"value\":\"dHJ1ZQ==\"}]}",
|
||||
appMetadata: AppMetadata{
|
||||
WTAccountID: "ok",
|
||||
WTPendingInvite: &invite,
|
||||
},
|
||||
statusCode: 200,
|
||||
helper: JsonParser{},
|
||||
managerCreds: &mockZitadelCredentials{
|
||||
jwtToken: JWTToken{},
|
||||
},
|
||||
assertErrFunc: assert.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2,
|
||||
updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
reqClient := mockHTTPClient{
|
||||
resBody: testCase.inputReqBody,
|
||||
code: testCase.statusCode,
|
||||
}
|
||||
|
||||
manager := &ZitadelManager{
|
||||
httpClient: &reqClient,
|
||||
credentials: testCase.managerCreds,
|
||||
helper: testCase.helper,
|
||||
}
|
||||
|
||||
err := manager.UpdateUserAppMetadata("1", testCase.appMetadata)
|
||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
||||
|
||||
assert.Equal(t, testCase.expectedReqBody, reqClient.reqBody, "request body should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestZitadelProfile(t *testing.T) {
|
||||
type azureProfileTest struct {
|
||||
name string
|
||||
@@ -418,16 +318,6 @@ func TestZitadelProfile(t *testing.T) {
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
},
|
||||
Metadata: []zitadelMetadata{
|
||||
{
|
||||
Key: "wt_account_id",
|
||||
Value: "MQ==",
|
||||
},
|
||||
{
|
||||
Key: "wt_pending_invite",
|
||||
Value: "ZmFsc2U=",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUserData: UserData{
|
||||
ID: "test1",
|
||||
@@ -451,16 +341,6 @@ func TestZitadelProfile(t *testing.T) {
|
||||
"machine",
|
||||
},
|
||||
Human: nil,
|
||||
Metadata: []zitadelMetadata{
|
||||
{
|
||||
Key: "wt_account_id",
|
||||
Value: "MQ==",
|
||||
},
|
||||
{
|
||||
Key: "wt_pending_invite",
|
||||
Value: "dHJ1ZQ==",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUserData: UserData{
|
||||
ID: "test2",
|
||||
@@ -480,8 +360,6 @@ func TestZitadelProfile(t *testing.T) {
|
||||
assert.Equal(t, testCase.expectedUserData.ID, userData.ID, "User id should match")
|
||||
assert.Equal(t, testCase.expectedUserData.Email, userData.Email, "User email should match")
|
||||
assert.Equal(t, testCase.expectedUserData.Name, userData.Name, "User name should match")
|
||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTAccountID, userData.AppMetadata.WTAccountID, "Account id should match")
|
||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTPendingInvite, userData.AppMetadata.WTPendingInvite, "Pending invite should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ func (w *Worker) generateProperties() properties {
|
||||
rulesDirection map[string]int
|
||||
groups int
|
||||
routes int
|
||||
routesWithRGGroups int
|
||||
nameservers int
|
||||
uiClient int
|
||||
version string
|
||||
@@ -201,6 +202,11 @@ func (w *Worker) generateProperties() properties {
|
||||
|
||||
groups = groups + len(account.Groups)
|
||||
routes = routes + len(account.Routes)
|
||||
for _, route := range account.Routes {
|
||||
if len(route.PeerGroups) > 0 {
|
||||
routesWithRGGroups++
|
||||
}
|
||||
}
|
||||
nameservers = nameservers + len(account.NameServerGroups)
|
||||
|
||||
for _, policy := range account.Policies {
|
||||
@@ -282,6 +288,7 @@ func (w *Worker) generateProperties() properties {
|
||||
metricsProperties["rules"] = rules
|
||||
metricsProperties["groups"] = groups
|
||||
metricsProperties["routes"] = routes
|
||||
metricsProperties["routes_with_routing_groups"] = routesWithRGGroups
|
||||
metricsProperties["nameservers"] = nameservers
|
||||
metricsProperties["version"] = version
|
||||
metricsProperties["min_active_peer_version"] = minActivePeerVersion
|
||||
|
||||
239
management/server/metrics/selfhosted_test.go
Normal file
239
management/server/metrics/selfhosted_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
type mockDatasource struct{}
|
||||
|
||||
// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information
|
||||
func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
|
||||
return map[string]struct{}{
|
||||
"1": {},
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllAccounts returns a list of *server.Account for use in tests with predefined information
|
||||
func (mockDatasource) GetAllAccounts() []*server.Account {
|
||||
return []*server.Account{
|
||||
{
|
||||
Id: "1",
|
||||
Settings: &server.Settings{PeerLoginExpirationEnabled: true},
|
||||
SetupKeys: map[string]*server.SetupKey{
|
||||
"1": {
|
||||
Id: "1",
|
||||
Ephemeral: true,
|
||||
UsedTimes: 1,
|
||||
},
|
||||
},
|
||||
Groups: map[string]*server.Group{
|
||||
"1": {},
|
||||
"2": {},
|
||||
},
|
||||
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
||||
"1": {},
|
||||
},
|
||||
Peers: map[string]*server.Peer{
|
||||
"1": {
|
||||
ID: "1",
|
||||
UserID: "test",
|
||||
SSHEnabled: true,
|
||||
Meta: server.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
|
||||
},
|
||||
},
|
||||
Policies: []*server.Policy{
|
||||
{
|
||||
Rules: []*server.PolicyRule{
|
||||
{
|
||||
Bidirectional: true,
|
||||
Protocol: server.PolicyRuleProtocolTCP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Rules: []*server.PolicyRule{
|
||||
{
|
||||
Bidirectional: false,
|
||||
Protocol: server.PolicyRuleProtocolTCP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Routes: map[string]*route.Route{
|
||||
"1": {
|
||||
ID: "1",
|
||||
PeerGroups: make([]string, 1),
|
||||
},
|
||||
},
|
||||
Users: map[string]*server.User{
|
||||
"1": {
|
||||
IsServiceUser: true,
|
||||
PATs: map[string]*server.PersonalAccessToken{
|
||||
"1": {},
|
||||
},
|
||||
},
|
||||
"2": {
|
||||
IsServiceUser: false,
|
||||
PATs: map[string]*server.PersonalAccessToken{
|
||||
"1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "2",
|
||||
Settings: &server.Settings{PeerLoginExpirationEnabled: true},
|
||||
SetupKeys: map[string]*server.SetupKey{
|
||||
"1": {
|
||||
Id: "1",
|
||||
Ephemeral: true,
|
||||
UsedTimes: 1,
|
||||
},
|
||||
},
|
||||
Groups: map[string]*server.Group{
|
||||
"1": {},
|
||||
"2": {},
|
||||
},
|
||||
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
||||
"1": {},
|
||||
},
|
||||
Peers: map[string]*server.Peer{
|
||||
"1": {
|
||||
ID: "1",
|
||||
UserID: "test",
|
||||
SSHEnabled: true,
|
||||
Meta: server.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
|
||||
},
|
||||
},
|
||||
Policies: []*server.Policy{
|
||||
{
|
||||
Rules: []*server.PolicyRule{
|
||||
{
|
||||
Bidirectional: true,
|
||||
Protocol: server.PolicyRuleProtocolTCP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Rules: []*server.PolicyRule{
|
||||
{
|
||||
Bidirectional: false,
|
||||
Protocol: server.PolicyRuleProtocolTCP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Routes: map[string]*route.Route{
|
||||
"1": {
|
||||
ID: "1",
|
||||
PeerGroups: make([]string, 1),
|
||||
},
|
||||
},
|
||||
Users: map[string]*server.User{
|
||||
"1": {
|
||||
IsServiceUser: true,
|
||||
PATs: map[string]*server.PersonalAccessToken{
|
||||
"1": {},
|
||||
},
|
||||
},
|
||||
"2": {
|
||||
IsServiceUser: false,
|
||||
PATs: map[string]*server.PersonalAccessToken{
|
||||
"1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties
|
||||
func TestGenerateProperties(t *testing.T) {
|
||||
ds := mockDatasource{}
|
||||
worker := Worker{
|
||||
dataSource: ds,
|
||||
connManager: ds,
|
||||
}
|
||||
|
||||
properties := worker.generateProperties()
|
||||
|
||||
if properties["accounts"] != 2 {
|
||||
t.Errorf("expected 2 accounts, got %d", properties["accounts"])
|
||||
}
|
||||
if properties["peers"] != 2 {
|
||||
t.Errorf("expected 2 peers, got %d", properties["peers"])
|
||||
}
|
||||
if properties["routes"] != 2 {
|
||||
t.Errorf("expected 2 routes, got %d", properties["routes"])
|
||||
}
|
||||
if properties["rules"] != 4 {
|
||||
t.Errorf("expected 4 rules, got %d", properties["rules"])
|
||||
}
|
||||
if properties["users"] != 2 {
|
||||
t.Errorf("expected 1 users, got %d", properties["users"])
|
||||
}
|
||||
if properties["setup_keys_usage"] != 2 {
|
||||
t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
|
||||
}
|
||||
if properties["pats"] != 4 {
|
||||
t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
|
||||
}
|
||||
if properties["peers_ssh_enabled"] != 2 {
|
||||
t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
|
||||
}
|
||||
if properties["routes_with_routing_groups"] != 2 {
|
||||
t.Errorf("expected 2 routes_with_routing_groups, got %d", properties["routes_with_routing_groups"])
|
||||
}
|
||||
if properties["rules_protocol_tcp"] != 4 {
|
||||
t.Errorf("expected 4 rules_protocol_tcp, got %d", properties["rules_protocol_tcp"])
|
||||
}
|
||||
if properties["rules_direction_oneway"] != 2 {
|
||||
t.Errorf("expected 2 rules_direction_oneway, got %d", properties["rules_direction_oneway"])
|
||||
}
|
||||
|
||||
if properties["active_peers_last_day"] != 2 {
|
||||
t.Errorf("expected 2 active_peers_last_day, got %d", properties["active_peers_last_day"])
|
||||
}
|
||||
if properties["min_active_peer_version"] != "0.0.1" {
|
||||
t.Errorf("expected 0.0.1 min_active_peer_version, got %s", properties["min_active_peer_version"])
|
||||
}
|
||||
if properties["max_active_peer_version"] != "0.0.1" {
|
||||
t.Errorf("expected 0.0.1 max_active_peer_version, got %s", properties["max_active_peer_version"])
|
||||
}
|
||||
|
||||
if properties["peers_login_expiration_enabled"] != 2 {
|
||||
t.Errorf("expected 2 peers_login_expiration_enabled, got %d", properties["peers_login_expiration_enabled"])
|
||||
}
|
||||
|
||||
if properties["service_users"] != 2 {
|
||||
t.Errorf("expected 2 service_users, got %d", properties["service_users"])
|
||||
}
|
||||
|
||||
if properties["peer_os_linux"] != 2 {
|
||||
t.Errorf("expected 2 peer_os_linux, got %d", properties["peer_os_linux"])
|
||||
}
|
||||
|
||||
if properties["ephemeral_peers_setup_keys"] != 2 {
|
||||
t.Errorf("expected 2 ephemeral_peers_setup_keys, got %d", properties["ephemeral_peers_setup_keys_usage"])
|
||||
}
|
||||
|
||||
if properties["ephemeral_peers_setup_keys_usage"] != 2 {
|
||||
t.Errorf("expected 2 ephemeral_peers_setup_keys_usage, got %d", properties["ephemeral_peers_setup_keys_usage"])
|
||||
}
|
||||
|
||||
if properties["nameservers"] != 2 {
|
||||
t.Errorf("expected 2 nameservers, got %d", properties["nameservers"])
|
||||
}
|
||||
|
||||
if properties["groups"] != 4 {
|
||||
t.Errorf("expected 4 groups, got %d", properties["groups"])
|
||||
}
|
||||
|
||||
if properties["user_peers"] != 2 {
|
||||
t.Errorf("expected 2 user_peers, got %d", properties["user_peers"])
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,9 @@ type MockAccountManager struct {
|
||||
GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error)
|
||||
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
|
||||
GetUserFunc func(claims jwtclaims.AuthorizationClaims) (*server.User, error)
|
||||
AccountExistsFunc func(accountId string) (*bool, error)
|
||||
GetPeerByKeyFunc func(peerKey string) (*server.Peer, error)
|
||||
GetPeersFunc func(accountID, userID string) ([]*server.Peer, error)
|
||||
MarkPeerConnectedFunc func(peerKey string, connected bool) error
|
||||
DeletePeerFunc func(accountID, peerKey, userID string) (*server.Peer, error)
|
||||
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
|
||||
DeletePeerFunc func(accountID, peerKey, userID string) error
|
||||
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
|
||||
GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
|
||||
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, *server.NetworkMap, error)
|
||||
@@ -35,7 +32,6 @@ type MockAccountManager struct {
|
||||
ListGroupsFunc func(accountID string) ([]*server.Group, error)
|
||||
GroupAddPeerFunc func(accountID, groupID, peerID string) error
|
||||
GroupDeletePeerFunc func(accountID, groupID, peerID string) error
|
||||
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
|
||||
GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error)
|
||||
SaveRuleFunc func(accountID, userID string, rule *server.Rule) error
|
||||
DeleteRuleFunc func(accountID, ruleID, userID string) error
|
||||
@@ -90,11 +86,11 @@ func (am *MockAccountManager) GetUsersFromAccount(accountID string, userID strin
|
||||
}
|
||||
|
||||
// DeletePeer mock implementation of DeletePeer from server.AccountManager interface
|
||||
func (am *MockAccountManager) DeletePeer(accountID, peerID, userID string) (*server.Peer, error) {
|
||||
func (am *MockAccountManager) DeletePeer(accountID, peerID, userID string) error {
|
||||
if am.DeletePeerFunc != nil {
|
||||
return am.DeletePeerFunc(accountID, peerID, userID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeletePeer is not implemented")
|
||||
return status.Errorf(codes.Unimplemented, "method DeletePeer is not implemented")
|
||||
}
|
||||
|
||||
// GetOrCreateAccountByUser mock implementation of GetOrCreateAccountByUser from server.AccountManager interface
|
||||
@@ -140,22 +136,6 @@ func (am *MockAccountManager) GetAccountByUserOrAccountID(
|
||||
)
|
||||
}
|
||||
|
||||
// AccountExists mock implementation of AccountExists from server.AccountManager interface
|
||||
func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) {
|
||||
if am.AccountExistsFunc != nil {
|
||||
return am.AccountExistsFunc(accountId)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AccountExists is not implemented")
|
||||
}
|
||||
|
||||
// GetPeerByKey mocks implementation of GetPeerByKey from server.AccountManager interface
|
||||
func (am *MockAccountManager) GetPeerByKey(peerKey string) (*server.Peer, error) {
|
||||
if am.GetPeerByKeyFunc != nil {
|
||||
return am.GetPeerByKeyFunc(peerKey)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPeerByKey is not implemented")
|
||||
}
|
||||
|
||||
// MarkPeerConnected mock implementation of MarkPeerConnected from server.AccountManager interface
|
||||
func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool) error {
|
||||
if am.MarkPeerConnectedFunc != nil {
|
||||
@@ -164,14 +144,6 @@ func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool)
|
||||
return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
|
||||
}
|
||||
|
||||
// GetPeerByIP mock implementation of GetPeerByIP from server.AccountManager interface
|
||||
func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*server.Peer, error) {
|
||||
if am.GetPeerByIPFunc != nil {
|
||||
return am.GetPeerByIPFunc(accountId, peerIP)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPeerByIP is not implemented")
|
||||
}
|
||||
|
||||
// GetAccountFromPAT mock implementation of GetAccountFromPAT from server.AccountManager interface
|
||||
func (am *MockAccountManager) GetAccountFromPAT(pat string) (*server.Account, *server.User, *server.PersonalAccessToken, error) {
|
||||
if am.GetAccountFromPATFunc != nil {
|
||||
@@ -296,14 +268,6 @@ func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerID string)
|
||||
return status.Errorf(codes.Unimplemented, "method GroupDeletePeer is not implemented")
|
||||
}
|
||||
|
||||
// GroupListPeers mock implementation of GroupListPeers from server.AccountManager interface
|
||||
func (am *MockAccountManager) GroupListPeers(accountID, groupID string) ([]*server.Peer, error) {
|
||||
if am.GroupListPeersFunc != nil {
|
||||
return am.GroupListPeersFunc(accountID, groupID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GroupListPeers is not implemented")
|
||||
}
|
||||
|
||||
// GetRule mock implementation of GetRule from server.AccountManager interface
|
||||
func (am *MockAccountManager) GetRule(accountID, ruleID, userID string) (*server.Rule, error) {
|
||||
if am.GetRuleFunc != nil {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
@@ -74,11 +73,7 @@ func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, d
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return newNSGroup.Copy(), status.Errorf(status.Internal, "failed to update peers after create nameserver %s", name)
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
am.storeEvent(userID, newNSGroup.ID, accountID, activity.NameserverGroupCreated, newNSGroup.EventMeta())
|
||||
|
||||
@@ -113,11 +108,7 @@ func (am *DefaultAccountManager) SaveNameServerGroup(accountID, userID string, n
|
||||
return err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return status.Errorf(status.Internal, "failed to update peers after update nameserver %s", nsGroupToSave.Name)
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
am.storeEvent(userID, nsGroupToSave.ID, accountID, activity.NameserverGroupUpdated, nsGroupToSave.EventMeta())
|
||||
|
||||
@@ -147,10 +138,7 @@ func (am *DefaultAccountManager) DeleteNameServerGroup(accountID, nsGroupID, use
|
||||
return err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return status.Errorf(status.Internal, "failed to update peers after deleting nameserver %s", nsGroupID)
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
am.storeEvent(userID, nsGroup.ID, accountID, activity.NameserverGroupDeleted, nsGroup.EventMeta())
|
||||
|
||||
|
||||
@@ -195,16 +195,6 @@ func (p *PeerStatus) Copy() *PeerStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// GetPeerByKey looks up peer by its public WireGuard key
|
||||
func (am *DefaultAccountManager) GetPeerByKey(peerPubKey string) (*Peer, error) {
|
||||
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return account.FindPeerByPubKey(peerPubKey)
|
||||
}
|
||||
|
||||
// GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
|
||||
// the current user is not an admin.
|
||||
func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, error) {
|
||||
@@ -290,10 +280,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected
|
||||
if oldStatus.LoginExpired {
|
||||
// we need to update other peers because when peer login expires all other peers are notified to disconnect from
|
||||
// the expired one. Here we notify them that connection is now allowed again.
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -364,82 +351,75 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Pe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// deletePeers will delete all specified peers and send updates to the remote peers. Don't call without acquiring account lock
|
||||
func (am *DefaultAccountManager) deletePeers(account *Account, peerIDs []string, userID string) error {
|
||||
|
||||
// the first loop is needed to ensure all peers present under the account before modifying, otherwise
|
||||
// we might have some inconsistencies
|
||||
peers := make([]*Peer, 0, len(peerIDs))
|
||||
for _, peerID := range peerIDs {
|
||||
|
||||
peer := account.GetPeer(peerID)
|
||||
if peer == nil {
|
||||
return status.Errorf(status.NotFound, "peer %s not found", peerID)
|
||||
}
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
|
||||
// the 2nd loop performs the actual modification
|
||||
for _, peer := range peers {
|
||||
account.DeletePeer(peer.ID)
|
||||
am.peersUpdateManager.SendUpdate(peer.ID,
|
||||
&UpdateMessage{
|
||||
Update: &proto.SyncResponse{
|
||||
// fill those field for backward compatibility
|
||||
RemotePeers: []*proto.RemotePeerConfig{},
|
||||
RemotePeersIsEmpty: true,
|
||||
// new field
|
||||
NetworkMap: &proto.NetworkMap{
|
||||
Serial: account.Network.CurrentSerial(),
|
||||
RemotePeers: []*proto.RemotePeerConfig{},
|
||||
RemotePeersIsEmpty: true,
|
||||
FirewallRules: []*proto.FirewallRule{},
|
||||
FirewallRulesIsEmpty: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
am.peersUpdateManager.CloseChannel(peer.ID)
|
||||
am.storeEvent(userID, peer.ID, account.Id, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePeer removes peer from the account by its IP
|
||||
func (am *DefaultAccountManager) DeletePeer(accountID, peerID, userID string) (*Peer, error) {
|
||||
func (am *DefaultAccountManager) DeletePeer(accountID, peerID, userID string) error {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
peer := account.GetPeer(peerID)
|
||||
if peer == nil {
|
||||
return nil, status.Errorf(status.NotFound, "peer %s not found", peerID)
|
||||
err = am.deletePeers(account, []string{peerID}, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account.DeletePeer(peerID)
|
||||
|
||||
err = am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
err = am.peersUpdateManager.SendUpdate(peer.ID,
|
||||
&UpdateMessage{
|
||||
Update: &proto.SyncResponse{
|
||||
// fill those field for backward compatibility
|
||||
RemotePeers: []*proto.RemotePeerConfig{},
|
||||
RemotePeersIsEmpty: true,
|
||||
// new field
|
||||
NetworkMap: &proto.NetworkMap{
|
||||
Serial: account.Network.CurrentSerial(),
|
||||
RemotePeers: []*proto.RemotePeerConfig{},
|
||||
RemotePeersIsEmpty: true,
|
||||
FirewallRules: []*proto.FirewallRule{},
|
||||
FirewallRulesIsEmpty: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
if err := am.updateAccountPeers(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
am.peersUpdateManager.CloseChannel(peerID)
|
||||
am.storeEvent(userID, peer.ID, account.Id, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain()))
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// GetPeerByIP returns peer by its IP
|
||||
func (am *DefaultAccountManager) GetPeerByIP(accountID string, peerIP string) (*Peer, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, peer := range account.Peers {
|
||||
if peerIP == peer.IP.String() {
|
||||
return peer, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, status.Errorf(status.NotFound, "peer with IP %s not found", peerIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
|
||||
@@ -609,10 +589,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
|
||||
am.storeEvent(opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
networkMap := account.GetPeerNetworkMap(newPeer.ID, am.dnsDomain)
|
||||
return newPeer, networkMap, nil
|
||||
@@ -727,10 +704,7 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap,
|
||||
}
|
||||
|
||||
if updateRemotePeers {
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
}
|
||||
return peer, account.GetPeerNetworkMap(peer.ID, am.dnsDomain), nil
|
||||
}
|
||||
@@ -804,10 +778,7 @@ func (am *DefaultAccountManager) checkAndUpdatePeerSSHKey(peer *Peer, account *A
|
||||
}
|
||||
|
||||
// trigger network map update
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return peer, nil
|
||||
}
|
||||
@@ -852,7 +823,9 @@ func (am *DefaultAccountManager) UpdatePeerSSHKey(peerID string, sshKey string)
|
||||
}
|
||||
|
||||
// trigger network map update
|
||||
return am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPeer for a given accountID, peerID and userID error if not found.
|
||||
@@ -909,21 +882,12 @@ func updatePeerMeta(peer *Peer, meta PeerSystemMeta, account *Account) (*Peer, b
|
||||
|
||||
// updateAccountPeers updates all peers that belong to an account.
|
||||
// Should be called when changes have to be synced to peers.
|
||||
func (am *DefaultAccountManager) updateAccountPeers(account *Account) error {
|
||||
func (am *DefaultAccountManager) updateAccountPeers(account *Account) {
|
||||
peers := account.GetPeers()
|
||||
|
||||
for _, peer := range peers {
|
||||
remotePeerNetworkMap, err := am.GetNetworkMap(peer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remotePeerNetworkMap := account.GetPeerNetworkMap(peer.ID, am.dnsDomain)
|
||||
update := toSyncResponse(nil, peer, nil, remotePeerNetworkMap, am.GetDNSDomain())
|
||||
err = am.peersUpdateManager.SendUpdate(peer.ID, &UpdateMessage{Update: update})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
am.peersUpdateManager.SendUpdate(peer.ID, &UpdateMessage{Update: update})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -350,7 +350,9 @@ func (am *DefaultAccountManager) SavePolicy(accountID, userID string, policy *Po
|
||||
}
|
||||
am.storeEvent(userID, policy.ID, accountID, action, policy.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePolicy from the store
|
||||
@@ -375,7 +377,9 @@ func (am *DefaultAccountManager) DeletePolicy(accountID, policyID, userID string
|
||||
|
||||
am.storeEvent(userID, policy.ID, accountID, activity.PolicyRemoved, policy.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListPolicies from the store
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetRoute gets a route object from account and route IDs
|
||||
@@ -185,11 +184,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return &newRoute, status.Errorf(status.Internal, "failed to update peers after create route %s", newPrefix)
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
am.storeEvent(userID, newRoute.ID, accountID, activity.RouteCreated, newRoute.EventMeta())
|
||||
|
||||
@@ -250,10 +245,7 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave
|
||||
return err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
am.storeEvent(userID, routeToSave.ID, accountID, activity.RouteUpdated, routeToSave.EventMeta())
|
||||
|
||||
@@ -283,7 +275,9 @@ func (am *DefaultAccountManager) DeleteRoute(accountID, routeID, userID string)
|
||||
|
||||
am.storeEvent(userID, routy.ID, accountID, activity.RouteRemoved, routy.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRoutes returns a list of routes from account
|
||||
|
||||
@@ -317,7 +317,9 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup
|
||||
}
|
||||
}()
|
||||
|
||||
return newKey, am.updateAccountPeers(account)
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return newKey, nil
|
||||
}
|
||||
|
||||
// ListSetupKeys returns a list of all setup keys of the account
|
||||
|
||||
@@ -118,11 +118,7 @@ func (m *TimeBasedAuthSecretsManager) SetupRefresh(peerID string) {
|
||||
},
|
||||
}
|
||||
log.Debugf("sending new TURN credentials to peer %s", peerID)
|
||||
err := m.updateManager.SendUpdate(peerID, &UpdateMessage{Update: update})
|
||||
if err != nil {
|
||||
log.Errorf("error while sending TURN update to peer %s %v", peerID, err)
|
||||
// todo maybe continue trying?
|
||||
}
|
||||
m.updateManager.SendUpdate(peerID, &UpdateMessage{Update: update})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewPeersUpdateManager() *PeersUpdateManager {
|
||||
}
|
||||
|
||||
// SendUpdate sends update message to the peer's channel
|
||||
func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) error {
|
||||
func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) {
|
||||
p.channelsMux.Lock()
|
||||
defer p.channelsMux.Unlock()
|
||||
if channel, ok := p.peerChannels[peerID]; ok {
|
||||
@@ -39,10 +39,9 @@ func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) er
|
||||
default:
|
||||
log.Warnf("channel for peer %s is %d full", peerID, len(channel))
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
log.Debugf("peer %s has no channel", peerID)
|
||||
}
|
||||
log.Debugf("peer %s has no channel", peerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateChannel creates a go channel for a given peer used to deliver updates relevant to the peer.
|
||||
|
||||
@@ -31,10 +31,7 @@ func TestSendUpdate(t *testing.T) {
|
||||
if _, ok := peersUpdater.peerChannels[peer]; !ok {
|
||||
t.Error("Error creating the channel")
|
||||
}
|
||||
err := peersUpdater.SendUpdate(peer, update1)
|
||||
if err != nil {
|
||||
t.Error("Error sending update: ", err)
|
||||
}
|
||||
peersUpdater.SendUpdate(peer, update1)
|
||||
select {
|
||||
case <-peersUpdater.peerChannels[peer]:
|
||||
default:
|
||||
@@ -42,10 +39,7 @@ func TestSendUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
for range [channelBufferSize]int{} {
|
||||
err = peersUpdater.SendUpdate(peer, update1)
|
||||
if err != nil {
|
||||
t.Errorf("got an early error sending update: %v ", err)
|
||||
}
|
||||
peersUpdater.SendUpdate(peer, update1)
|
||||
}
|
||||
|
||||
update2 := &UpdateMessage{Update: &proto.SyncResponse{
|
||||
@@ -54,10 +48,7 @@ func TestSendUpdate(t *testing.T) {
|
||||
},
|
||||
}}
|
||||
|
||||
err = peersUpdater.SendUpdate(peer, update2)
|
||||
if err != nil {
|
||||
t.Error("update shouldn't return an error when channel buffer is full")
|
||||
}
|
||||
peersUpdater.SendUpdate(peer, update2)
|
||||
timeout := time.After(5 * time.Second)
|
||||
for range [channelBufferSize]int{} {
|
||||
select {
|
||||
|
||||
@@ -307,6 +307,12 @@ func (am *DefaultAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) (
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) deleteServiceUser(account *Account, initiatorUserID string, targetUser *User) {
|
||||
meta := map[string]any{"name": targetUser.ServiceUserName}
|
||||
am.storeEvent(initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta)
|
||||
delete(account.Users, targetUser.Id)
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user from the given account.
|
||||
func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, targetUserID string) error {
|
||||
if initiatorUserID == targetUserID {
|
||||
@@ -320,11 +326,6 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t
|
||||
return err
|
||||
}
|
||||
|
||||
targetUser := account.Users[targetUserID]
|
||||
if targetUser == nil {
|
||||
return status.Errorf(status.NotFound, "user not found")
|
||||
}
|
||||
|
||||
executingUser := account.Users[initiatorUserID]
|
||||
if executingUser == nil {
|
||||
return status.Errorf(status.NotFound, "user not found")
|
||||
@@ -333,51 +334,68 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t
|
||||
return status.Errorf(status.PermissionDenied, "only admins can delete users")
|
||||
}
|
||||
|
||||
peers, err := account.FindUserPeers(targetUserID)
|
||||
if err != nil {
|
||||
return status.Errorf(status.Internal, "failed to find user peers")
|
||||
targetUser := account.Users[targetUserID]
|
||||
if targetUser == nil {
|
||||
return status.Errorf(status.NotFound, "target user not found")
|
||||
}
|
||||
|
||||
if err := am.expireAndUpdatePeers(account, peers); err != nil {
|
||||
log.Errorf("failed update deleted peers expiration: %s", err)
|
||||
return err
|
||||
// handle service user first and exit, no need to fetch extra data from IDP, etc
|
||||
if targetUser.IsServiceUser {
|
||||
am.deleteServiceUser(account, initiatorUserID, targetUser)
|
||||
return am.Store.SaveAccount(account)
|
||||
}
|
||||
|
||||
return am.deleteRegularUser(account, initiatorUserID, targetUserID)
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) deleteRegularUser(account *Account, initiatorUserID, targetUserID string) error {
|
||||
tuEmail, tuName, err := am.getEmailAndNameOfTargetUser(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{"name": tuName, "email": tuEmail}
|
||||
eventAction = activity.UserDeleted
|
||||
}
|
||||
am.storeEvent(initiatorUserID, targetUserID, accountID, eventAction, meta)
|
||||
|
||||
if !targetUser.IsServiceUser && !isNil(am.idpManager) {
|
||||
err := am.deleteUserFromIDP(targetUserID, accountID)
|
||||
if !isNil(am.idpManager) {
|
||||
err = am.deleteUserFromIDP(targetUserID, account.Id)
|
||||
if err != nil {
|
||||
log.Debugf("failed to delete user from IDP: %s", targetUserID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
delete(account.Users, targetUserID)
|
||||
err = am.deleteUserPeers(initiatorUserID, targetUserID, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(account.Users, targetUserID)
|
||||
err = am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
meta := map[string]any{"name": tuName, "email": tuEmail}
|
||||
am.storeEvent(initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta)
|
||||
|
||||
am.updateAccountPeers(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) deleteUserPeers(initiatorUserID string, targetUserID string, account *Account) error {
|
||||
peers, err := account.FindUserPeers(targetUserID)
|
||||
if err != nil {
|
||||
return status.Errorf(status.Internal, "failed to find user peers")
|
||||
}
|
||||
|
||||
peerIDs := make([]string, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
peerIDs = append(peerIDs, peer.ID)
|
||||
}
|
||||
|
||||
return am.deletePeers(account, peerIDs, initiatorUserID)
|
||||
}
|
||||
|
||||
// InviteUser resend invitations to users who haven't activated their accounts prior to the expiration period.
|
||||
func (am *DefaultAccountManager) InviteUser(accountID string, initiatorUserID string, targetUserID string) error {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
@@ -658,9 +676,7 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := am.updateAccountPeers(account); err != nil {
|
||||
log.Errorf("failed updating account peers while updating user %s", accountID)
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
} else {
|
||||
if err = am.Store.SaveAccount(account); err != nil {
|
||||
return nil, err
|
||||
@@ -854,9 +870,7 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(account *Account, peers []
|
||||
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
|
||||
}
|
||||
am.updateAccountPeers(account)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user