mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-05 08:36:37 +00:00
[management] Replace JumpCloud SDK with direct HTTP calls (#5591)
This commit is contained in:
1
go.mod
1
go.mod
@@ -30,7 +30,6 @@ require (
|
|||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.7.0
|
fyne.io/fyne/v2 v2.7.0
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
||||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible
|
|
||||||
github.com/awnumar/memguard v0.23.0
|
github.com/awnumar/memguard v0.23.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.3
|
github.com/aws/aws-sdk-go-v2 v1.36.3
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -34,8 +34,6 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
|
|||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||||
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/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetr
|
|||||||
case "jumpcloud":
|
case "jumpcloud":
|
||||||
return NewJumpCloudManager(JumpCloudClientConfig{
|
return NewJumpCloudManager(JumpCloudClientConfig{
|
||||||
APIToken: config.ExtraConfig["ApiToken"],
|
APIToken: config.ExtraConfig["ApiToken"],
|
||||||
|
ApiUrl: config.ExtraConfig["ApiUrl"],
|
||||||
}, appMetrics)
|
}, appMetrics)
|
||||||
case "pocketid":
|
case "pocketid":
|
||||||
return NewPocketIdManager(PocketIdClientConfig{
|
return NewPocketIdManager(PocketIdClientConfig{
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
package idp
|
package idp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
v1 "github.com/TheJumpCloud/jcapi-go/v1"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
contentType = "application/json"
|
jumpCloudDefaultApiUrl = "https://console.jumpcloud.com"
|
||||||
accept = "application/json"
|
jumpCloudSearchPageSize = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// jumpCloudUser represents a JumpCloud V1 API system user.
|
||||||
|
type jumpCloudUser struct {
|
||||||
|
ID string `json:"_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Firstname string `json:"firstname"`
|
||||||
|
Middlename string `json:"middlename"`
|
||||||
|
Lastname string `json:"lastname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// jumpCloudUserList represents the response from the JumpCloud search endpoint.
|
||||||
|
type jumpCloudUserList struct {
|
||||||
|
Results []jumpCloudUser `json:"results"`
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
}
|
||||||
|
|
||||||
// JumpCloudManager JumpCloud manager client instance.
|
// JumpCloudManager JumpCloud manager client instance.
|
||||||
type JumpCloudManager struct {
|
type JumpCloudManager struct {
|
||||||
client *v1.APIClient
|
apiBase string
|
||||||
apiToken string
|
apiToken string
|
||||||
httpClient ManagerHTTPClient
|
httpClient ManagerHTTPClient
|
||||||
credentials ManagerCredentials
|
credentials ManagerCredentials
|
||||||
@@ -29,6 +45,7 @@ type JumpCloudManager struct {
|
|||||||
// JumpCloudClientConfig JumpCloud manager client configurations.
|
// JumpCloudClientConfig JumpCloud manager client configurations.
|
||||||
type JumpCloudClientConfig struct {
|
type JumpCloudClientConfig struct {
|
||||||
APIToken string
|
APIToken string
|
||||||
|
ApiUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
// JumpCloudCredentials JumpCloud authentication information.
|
// JumpCloudCredentials JumpCloud authentication information.
|
||||||
@@ -55,7 +72,15 @@ func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppM
|
|||||||
return nil, fmt.Errorf("jumpCloud IdP configuration is incomplete, ApiToken is missing")
|
return nil, fmt.Errorf("jumpCloud IdP configuration is incomplete, ApiToken is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
client := v1.NewAPIClient(v1.NewConfiguration())
|
apiBase := config.ApiUrl
|
||||||
|
if apiBase == "" {
|
||||||
|
apiBase = jumpCloudDefaultApiUrl
|
||||||
|
}
|
||||||
|
apiBase = strings.TrimSuffix(apiBase, "/")
|
||||||
|
if !strings.HasSuffix(apiBase, "/api") {
|
||||||
|
apiBase += "/api"
|
||||||
|
}
|
||||||
|
|
||||||
credentials := &JumpCloudCredentials{
|
credentials := &JumpCloudCredentials{
|
||||||
clientConfig: config,
|
clientConfig: config,
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
@@ -64,7 +89,7 @@ func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppM
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &JumpCloudManager{
|
return &JumpCloudManager{
|
||||||
client: client,
|
apiBase: apiBase,
|
||||||
apiToken: config.APIToken,
|
apiToken: config.APIToken,
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
@@ -78,37 +103,58 @@ func (jc *JumpCloudCredentials) Authenticate(_ context.Context) (JWTToken, error
|
|||||||
return JWTToken{}, nil
|
return JWTToken{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jm *JumpCloudManager) authenticationContext() context.Context {
|
// doRequest executes an HTTP request against the JumpCloud V1 API.
|
||||||
return context.WithValue(context.Background(), v1.ContextAPIKey, v1.APIKey{
|
func (jm *JumpCloudManager) doRequest(ctx context.Context, method, path string, body io.Reader) ([]byte, error) {
|
||||||
Key: jm.apiToken,
|
reqURL := jm.apiBase + path
|
||||||
})
|
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
|
||||||
func (jm *JumpCloudManager) UpdateUserAppMetadata(_ context.Context, _ string, _ AppMetadata) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserDataByID requests user data from JumpCloud via ID.
|
|
||||||
func (jm *JumpCloudManager) GetUserDataByID(_ context.Context, userID string, appMetadata AppMetadata) (*UserData, error) {
|
|
||||||
authCtx := jm.authenticationContext()
|
|
||||||
user, resp, err := jm.client.SystemusersApi.SystemusersGet(authCtx, userID, contentType, accept, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.Header.Set("x-api-key", jm.apiToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := jm.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountRequestError()
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
if jm.appMetrics != nil {
|
if jm.appMetrics != nil {
|
||||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
return nil, fmt.Errorf("JumpCloud API request %s %s failed with status %d", method, path, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||||
|
func (jm *JumpCloudManager) UpdateUserAppMetadata(_ context.Context, _ string, _ AppMetadata) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserDataByID requests user data from JumpCloud via ID.
|
||||||
|
func (jm *JumpCloudManager) GetUserDataByID(ctx context.Context, userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||||
|
body, err := jm.doRequest(ctx, http.MethodGet, "/systemusers/"+userID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if jm.appMetrics != nil {
|
if jm.appMetrics != nil {
|
||||||
jm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
jm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var user jumpCloudUser
|
||||||
|
if err = jm.helper.Unmarshal(body, &user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
userData := parseJumpCloudUser(user)
|
userData := parseJumpCloudUser(user)
|
||||||
userData.AppMetadata = appMetadata
|
userData.AppMetadata = appMetadata
|
||||||
|
|
||||||
@@ -116,30 +162,20 @@ func (jm *JumpCloudManager) GetUserDataByID(_ context.Context, userID string, ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount returns all the users for a given profile.
|
// GetAccount returns all the users for a given profile.
|
||||||
func (jm *JumpCloudManager) GetAccount(_ context.Context, accountID string) ([]*UserData, error) {
|
func (jm *JumpCloudManager) GetAccount(ctx context.Context, accountID string) ([]*UserData, error) {
|
||||||
authCtx := jm.authenticationContext()
|
allUsers, err := jm.searchAllUsers(ctx)
|
||||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if jm.appMetrics != nil {
|
||||||
jm.appMetrics.IDPMetrics().CountGetAccount()
|
jm.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
users := make([]*UserData, 0)
|
users := make([]*UserData, 0, len(allUsers))
|
||||||
for _, user := range userList.Results {
|
for _, user := range allUsers {
|
||||||
userData := parseJumpCloudUser(user)
|
userData := parseJumpCloudUser(user)
|
||||||
userData.AppMetadata.WTAccountID = accountID
|
userData.AppMetadata.WTAccountID = accountID
|
||||||
|
|
||||||
users = append(users, userData)
|
users = append(users, userData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,27 +184,18 @@ func (jm *JumpCloudManager) GetAccount(_ context.Context, accountID string) ([]*
|
|||||||
|
|
||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
// It returns a list of users indexed by accountID.
|
// It returns a list of users indexed by accountID.
|
||||||
func (jm *JumpCloudManager) GetAllAccounts(_ context.Context) (map[string][]*UserData, error) {
|
func (jm *JumpCloudManager) GetAllAccounts(ctx context.Context) (map[string][]*UserData, error) {
|
||||||
authCtx := jm.authenticationContext()
|
allUsers, err := jm.searchAllUsers(ctx)
|
||||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if jm.appMetrics != nil {
|
||||||
jm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
jm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
indexedUsers := make(map[string][]*UserData)
|
||||||
for _, user := range userList.Results {
|
for _, user := range allUsers {
|
||||||
userData := parseJumpCloudUser(user)
|
userData := parseJumpCloudUser(user)
|
||||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||||
}
|
}
|
||||||
@@ -176,6 +203,41 @@ func (jm *JumpCloudManager) GetAllAccounts(_ context.Context) (map[string][]*Use
|
|||||||
return indexedUsers, nil
|
return indexedUsers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchAllUsers paginates through all system users using limit/skip.
|
||||||
|
func (jm *JumpCloudManager) searchAllUsers(ctx context.Context) ([]jumpCloudUser, error) {
|
||||||
|
var allUsers []jumpCloudUser
|
||||||
|
|
||||||
|
for skip := 0; ; skip += jumpCloudSearchPageSize {
|
||||||
|
searchReq := map[string]int{
|
||||||
|
"limit": jumpCloudSearchPageSize,
|
||||||
|
"skip": skip,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(searchReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := jm.doRequest(ctx, http.MethodPost, "/search/systemusers", bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var userList jumpCloudUserList
|
||||||
|
if err = jm.helper.Unmarshal(body, &userList); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsers = append(allUsers, userList.Results...)
|
||||||
|
|
||||||
|
if skip+len(userList.Results) >= userList.TotalCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in JumpCloud Idp and sends an invitation.
|
// CreateUser creates a new user in JumpCloud Idp and sends an invitation.
|
||||||
func (jm *JumpCloudManager) CreateUser(_ context.Context, _, _, _, _ string) (*UserData, error) {
|
func (jm *JumpCloudManager) CreateUser(_ context.Context, _, _, _, _ string) (*UserData, error) {
|
||||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
@@ -183,7 +245,7 @@ func (jm *JumpCloudManager) CreateUser(_ context.Context, _, _, _, _ string) (*U
|
|||||||
|
|
||||||
// GetUserByEmail searches users with a given email.
|
// GetUserByEmail searches users with a given email.
|
||||||
// If no users have been found, this function returns an empty list.
|
// If no users have been found, this function returns an empty list.
|
||||||
func (jm *JumpCloudManager) GetUserByEmail(_ context.Context, email string) ([]*UserData, error) {
|
func (jm *JumpCloudManager) GetUserByEmail(ctx context.Context, email string) ([]*UserData, error) {
|
||||||
searchFilter := map[string]interface{}{
|
searchFilter := map[string]interface{}{
|
||||||
"searchFilter": map[string]interface{}{
|
"searchFilter": map[string]interface{}{
|
||||||
"filter": []string{email},
|
"filter": []string{email},
|
||||||
@@ -191,25 +253,26 @@ func (jm *JumpCloudManager) GetUserByEmail(_ context.Context, email string) ([]*
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
authCtx := jm.authenticationContext()
|
payload, err := json.Marshal(searchFilter)
|
||||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, searchFilter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
body, err := jm.doRequest(ctx, http.MethodPost, "/search/systemusers", bytes.NewReader(payload))
|
||||||
if jm.appMetrics != nil {
|
if err != nil {
|
||||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
return nil, err
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", email, resp.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if jm.appMetrics != nil {
|
if jm.appMetrics != nil {
|
||||||
jm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
jm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
||||||
}
|
}
|
||||||
|
|
||||||
usersData := make([]*UserData, 0)
|
var userList jumpCloudUserList
|
||||||
|
if err = jm.helper.Unmarshal(body, &userList); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
usersData := make([]*UserData, 0, len(userList.Results))
|
||||||
for _, user := range userList.Results {
|
for _, user := range userList.Results {
|
||||||
usersData = append(usersData, parseJumpCloudUser(user))
|
usersData = append(usersData, parseJumpCloudUser(user))
|
||||||
}
|
}
|
||||||
@@ -224,20 +287,11 @@ func (jm *JumpCloudManager) InviteUserByID(_ context.Context, _ string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser from jumpCloud directory
|
// DeleteUser from jumpCloud directory
|
||||||
func (jm *JumpCloudManager) DeleteUser(_ context.Context, userID string) error {
|
func (jm *JumpCloudManager) DeleteUser(ctx context.Context, userID string) error {
|
||||||
authCtx := jm.authenticationContext()
|
_, err := jm.doRequest(ctx, http.MethodDelete, "/systemusers/"+userID, nil)
|
||||||
_, resp, err := jm.client.SystemusersApi.SystemusersDelete(authCtx, userID, contentType, accept, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if jm.appMetrics != nil {
|
||||||
jm.appMetrics.IDPMetrics().CountDeleteUser()
|
jm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
@@ -247,11 +301,11 @@ func (jm *JumpCloudManager) DeleteUser(_ context.Context, userID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseJumpCloudUser parse JumpCloud system user returned from API V1 to UserData.
|
// parseJumpCloudUser parse JumpCloud system user returned from API V1 to UserData.
|
||||||
func parseJumpCloudUser(user v1.Systemuserreturn) *UserData {
|
func parseJumpCloudUser(user jumpCloudUser) *UserData {
|
||||||
names := []string{user.Firstname, user.Middlename, user.Lastname}
|
names := []string{user.Firstname, user.Middlename, user.Lastname}
|
||||||
return &UserData{
|
return &UserData{
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Name: strings.Join(names, " "),
|
Name: strings.Join(names, " "),
|
||||||
ID: user.Id,
|
ID: user.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package idp
|
package idp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
@@ -44,3 +51,212 @@ func TestNewJumpCloudManager(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJumpCloudGetUserDataByID(t *testing.T) {
|
||||||
|
userResponse := jumpCloudUser{
|
||||||
|
ID: "user123",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Firstname: "John",
|
||||||
|
Middlename: "",
|
||||||
|
Lastname: "Doe",
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "/systemusers/user123", r.URL.Path)
|
||||||
|
assert.Equal(t, http.MethodGet, r.Method)
|
||||||
|
assert.Equal(t, "test-api-key", r.Header.Get("x-api-key"))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(userResponse)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
manager := newTestJumpCloudManager(t, server.URL)
|
||||||
|
|
||||||
|
userData, err := manager.GetUserDataByID(context.Background(), "user123", AppMetadata{WTAccountID: "acc1"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "user123", userData.ID)
|
||||||
|
assert.Equal(t, "test@example.com", userData.Email)
|
||||||
|
assert.Equal(t, "John Doe", userData.Name)
|
||||||
|
assert.Equal(t, "acc1", userData.AppMetadata.WTAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJumpCloudGetAccount(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "/search/systemusers", r.URL.Path)
|
||||||
|
assert.Equal(t, http.MethodPost, r.Method)
|
||||||
|
|
||||||
|
var reqBody map[string]any
|
||||||
|
assert.NoError(t, json.NewDecoder(r.Body).Decode(&reqBody))
|
||||||
|
assert.Contains(t, reqBody, "limit")
|
||||||
|
assert.Contains(t, reqBody, "skip")
|
||||||
|
|
||||||
|
resp := jumpCloudUserList{
|
||||||
|
Results: []jumpCloudUser{
|
||||||
|
{ID: "u1", Email: "a@test.com", Firstname: "Alice", Lastname: "Smith"},
|
||||||
|
{ID: "u2", Email: "b@test.com", Firstname: "Bob", Lastname: "Jones"},
|
||||||
|
},
|
||||||
|
TotalCount: 2,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
manager := newTestJumpCloudManager(t, server.URL)
|
||||||
|
|
||||||
|
users, err := manager.GetAccount(context.Background(), "testAccount")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, users, 2)
|
||||||
|
assert.Equal(t, "testAccount", users[0].AppMetadata.WTAccountID)
|
||||||
|
assert.Equal(t, "testAccount", users[1].AppMetadata.WTAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJumpCloudGetAllAccounts(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := jumpCloudUserList{
|
||||||
|
Results: []jumpCloudUser{
|
||||||
|
{ID: "u1", Email: "a@test.com", Firstname: "Alice"},
|
||||||
|
{ID: "u2", Email: "b@test.com", Firstname: "Bob"},
|
||||||
|
},
|
||||||
|
TotalCount: 2,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
manager := newTestJumpCloudManager(t, server.URL)
|
||||||
|
|
||||||
|
indexedUsers, err := manager.GetAllAccounts(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, indexedUsers[UnsetAccountID], 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJumpCloudGetAllAccountsPagination(t *testing.T) {
|
||||||
|
totalUsers := 250
|
||||||
|
allUsers := make([]jumpCloudUser, totalUsers)
|
||||||
|
for i := range allUsers {
|
||||||
|
allUsers[i] = jumpCloudUser{
|
||||||
|
ID: fmt.Sprintf("u%d", i),
|
||||||
|
Email: fmt.Sprintf("user%d@test.com", i),
|
||||||
|
Firstname: fmt.Sprintf("User%d", i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCount := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var reqBody map[string]int
|
||||||
|
assert.NoError(t, json.NewDecoder(r.Body).Decode(&reqBody))
|
||||||
|
|
||||||
|
limit := reqBody["limit"]
|
||||||
|
skip := reqBody["skip"]
|
||||||
|
requestCount++
|
||||||
|
|
||||||
|
end := skip + limit
|
||||||
|
if end > totalUsers {
|
||||||
|
end = totalUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := jumpCloudUserList{
|
||||||
|
Results: allUsers[skip:end],
|
||||||
|
TotalCount: totalUsers,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
manager := newTestJumpCloudManager(t, server.URL)
|
||||||
|
|
||||||
|
indexedUsers, err := manager.GetAllAccounts(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, indexedUsers[UnsetAccountID], totalUsers)
|
||||||
|
assert.Equal(t, 3, requestCount, "should require 3 pages for 250 users at page size 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJumpCloudGetUserByEmail(t *testing.T) {
|
||||||
|
searchResponse := jumpCloudUserList{
|
||||||
|
Results: []jumpCloudUser{
|
||||||
|
{ID: "u1", Email: "alice@test.com", Firstname: "Alice", Lastname: "Smith"},
|
||||||
|
},
|
||||||
|
TotalCount: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "/search/systemusers", r.URL.Path)
|
||||||
|
assert.Equal(t, http.MethodPost, r.Method)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, string(body), "alice@test.com")
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(searchResponse)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
manager := newTestJumpCloudManager(t, server.URL)
|
||||||
|
|
||||||
|
users, err := manager.GetUserByEmail(context.Background(), "alice@test.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, users, 1)
|
||||||
|
assert.Equal(t, "alice@test.com", users[0].Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJumpCloudDeleteUser(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "/systemusers/user123", r.URL.Path)
|
||||||
|
assert.Equal(t, http.MethodDelete, r.Method)
|
||||||
|
assert.Equal(t, "test-api-key", r.Header.Get("x-api-key"))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"_id": "user123"})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
manager := newTestJumpCloudManager(t, server.URL)
|
||||||
|
|
||||||
|
err := manager.DeleteUser(context.Background(), "user123")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJumpCloudAPIError(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
manager := newTestJumpCloudManager(t, server.URL)
|
||||||
|
|
||||||
|
_, err := manager.GetUserDataByID(context.Background(), "user123", AppMetadata{})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "401")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseJumpCloudUser(t *testing.T) {
|
||||||
|
user := jumpCloudUser{
|
||||||
|
ID: "abc123",
|
||||||
|
Email: "test@example.com",
|
||||||
|
Firstname: "John",
|
||||||
|
Middlename: "M",
|
||||||
|
Lastname: "Doe",
|
||||||
|
}
|
||||||
|
|
||||||
|
userData := parseJumpCloudUser(user)
|
||||||
|
assert.Equal(t, "abc123", userData.ID)
|
||||||
|
assert.Equal(t, "test@example.com", userData.Email)
|
||||||
|
assert.Equal(t, "John M Doe", userData.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestJumpCloudManager(t *testing.T, apiBase string) *JumpCloudManager {
|
||||||
|
t.Helper()
|
||||||
|
return &JumpCloudManager{
|
||||||
|
apiBase: apiBase,
|
||||||
|
apiToken: "test-api-key",
|
||||||
|
httpClient: http.DefaultClient,
|
||||||
|
helper: JsonParser{},
|
||||||
|
appMetrics: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user