Compare commits

...

16 Commits

Author SHA1 Message Date
bcmmbaga
59541b5c20 Merge branch 'main' into update-api-spec 2026-03-25 23:56:33 +03:00
bcmmbaga
75125883df add missing types
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
2026-03-25 14:38:17 +03:00
bcmmbaga
d8ca49b6de fix tags
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
2026-03-25 14:10:10 +03:00
bcmmbaga
50fc47200a refactor
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
2026-03-25 13:59:15 +03:00
bcmmbaga
b75274f7e7 fix casing
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
2026-03-25 13:49:58 +03:00
bcmmbaga
fa9c1c3b24 add rest client
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
2026-03-25 13:36:16 +03:00
bcmmbaga
86f3e9ec78 Refactor
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
2026-03-25 13:20:24 +03:00
Viktor Liu
9aaa05e8ea Replace discontinued LocalStack image with MinIO in S3 test (#5680) 2026-03-25 15:51:29 +08:00
Bethuel Mmbaga
0af5a0441f [management] Fix DNS label uniqueness check on peer rename (#5679) 2026-03-24 20:25:29 +03:00
Viktor Liu
0fc63ea0ba [management] Allow multiple header auths with same header name (#5678) 2026-03-24 16:18:21 +01:00
bcmmbaga
c935639a96 Merge branch 'main' into update-api-spec 2026-03-24 16:17:41 +03:00
Bethuel Mmbaga
0b329f7881 [management] Replace JumpCloud SDK with direct HTTP calls (#5591) 2026-03-24 13:21:42 +03:00
Viktor Liu
5b85edb753 [management] Omit proxy_protocol from API response when false (#5656)
The internal Target model uses a plain bool for ProxyProtocol,
which was always serialized to the API response as false even
when not configured. Only set the API field when true so it
gets omitted via omitempty when unset.
2026-03-23 17:53:17 +01:00
Maycon Santos
17cfa5fe1e [misc] Set signing env only if not fork and set license (#5659)
* Add condition to GPG key decoding to handle pull requests

* Add license field to deb and rpm package configurations

* Add condition to GPG key decoding for external pull requests
2026-03-23 17:16:23 +01:00
Viktor Liu
2313494e0e [client] Don't abort debug for command when up/down fails (#5657) 2026-03-23 14:04:03 +01:00
bcmmbaga
afe383fa2d add missing edr and idp sync endpoints
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
2026-03-17 18:47:29 +03:00
23 changed files with 3200 additions and 240 deletions

View File

@@ -170,6 +170,7 @@ jobs:
run: sudo apt update && sudo apt install -y -q gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
- name: Decode GPG signing key
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
env:
GPG_RPM_PRIVATE_KEY: ${{ secrets.GPG_RPM_PRIVATE_KEY }}
run: |
@@ -309,6 +310,7 @@ jobs:
run: sudo apt update && sudo apt install -y -q libappindicator3-dev gir1.2-appindicator3-0.1 libxxf86vm-dev gcc-mingw-w64-x86-64
- name: Decode GPG signing key
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
env:
GPG_RPM_PRIVATE_KEY: ${{ secrets.GPG_RPM_PRIVATE_KEY }}
run: |

View File

@@ -171,6 +171,7 @@ nfpms:
- maintainer: Netbird <dev@netbird.io>
description: Netbird client.
homepage: https://netbird.io/
license: BSD-3-Clause
id: netbird_deb
bindir: /usr/bin
builds:
@@ -184,6 +185,7 @@ nfpms:
- maintainer: Netbird <dev@netbird.io>
description: Netbird client.
homepage: https://netbird.io/
license: BSD-3-Clause
id: netbird_rpm
bindir: /usr/bin
builds:

View File

@@ -181,10 +181,11 @@ func runForDuration(cmd *cobra.Command, args []string) error {
if stateWasDown {
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
cmd.PrintErrf("Failed to bring service up: %v\n", status.Convert(err).Message())
} else {
cmd.Println("netbird up")
time.Sleep(time.Second * 10)
}
cmd.Println("netbird up")
time.Sleep(time.Second * 10)
}
initialLevelTrace := initialLogLevel.GetLevel() >= proto.LogLevel_TRACE
@@ -199,9 +200,10 @@ func runForDuration(cmd *cobra.Command, args []string) error {
}
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
cmd.PrintErrf("Failed to bring service down: %v\n", status.Convert(err).Message())
} else {
cmd.Println("netbird down")
}
cmd.Println("netbird down")
time.Sleep(1 * time.Second)
@@ -209,13 +211,14 @@ func runForDuration(cmd *cobra.Command, args []string) error {
if _, err := client.SetSyncResponsePersistence(cmd.Context(), &proto.SetSyncResponsePersistenceRequest{
Enabled: true,
}); err != nil {
return fmt.Errorf("failed to enable sync response persistence: %v", status.Convert(err).Message())
cmd.PrintErrf("Failed to enable sync response persistence: %v\n", status.Convert(err).Message())
}
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
cmd.PrintErrf("Failed to bring service up: %v\n", status.Convert(err).Message())
} else {
cmd.Println("netbird up")
}
cmd.Println("netbird up")
time.Sleep(3 * time.Second)
@@ -263,16 +266,18 @@ func runForDuration(cmd *cobra.Command, args []string) error {
if stateWasDown {
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
cmd.PrintErrf("Failed to restore service down state: %v\n", status.Convert(err).Message())
} else {
cmd.Println("netbird down")
}
cmd.Println("netbird down")
}
if !initialLevelTrace {
if _, err := client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{Level: initialLogLevel.GetLevel()}); err != nil {
return fmt.Errorf("failed to restore log level: %v", status.Convert(err).Message())
cmd.PrintErrf("Failed to restore log level: %v\n", status.Convert(err).Message())
} else {
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
}
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
}
cmd.Printf("Local file:\n%s\n", resp.GetPath())

3
go.mod
View File

@@ -30,10 +30,10 @@ require (
require (
fyne.io/fyne/v2 v2.7.0
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/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/credentials v1.17.67
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2
github.com/c-robinson/iplib v1.0.3
github.com/caddyserver/certmagic v0.21.3
@@ -144,7 +144,6 @@ require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/awnumar/memcall v0.4.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect

2
go.sum
View File

@@ -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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=

View File

@@ -262,7 +262,9 @@ func (s *Service) ToAPIResponse() *api.Service {
if opts == nil {
opts = &api.ServiceTargetOptions{}
}
opts.ProxyProtocol = &target.ProxyProtocol
if target.ProxyProtocol {
opts.ProxyProtocol = &target.ProxyProtocol
}
st.Options = opts
apiTargets = append(apiTargets, st)
}
@@ -848,7 +850,7 @@ func IsPortBasedProtocol(mode string) bool {
}
const (
maxCustomHeaders = 16
maxCustomHeaders = 16
maxHeaderKeyLen = 128
maxHeaderValueLen = 4096
)
@@ -945,7 +947,6 @@ func containsCRLF(s string) bool {
}
func validateHeaderAuths(headers []*HeaderAuthConfig) error {
seen := make(map[string]struct{})
for i, h := range headers {
if h == nil || !h.Enabled {
continue
@@ -966,10 +967,6 @@ func validateHeaderAuths(headers []*HeaderAuthConfig) error {
if canonical == "Host" {
return fmt.Errorf("header_auths[%d]: Host header cannot be used for auth", i)
}
if _, dup := seen[canonical]; dup {
return fmt.Errorf("header_auths[%d]: duplicate header %q (same canonical form already configured)", i, h.Header)
}
seen[canonical] = struct{}{}
if len(h.Value) > maxHeaderValueLen {
return fmt.Errorf("header_auths[%d]: value exceeds maximum length of %d", i, maxHeaderValueLen)
}

View File

@@ -935,3 +935,107 @@ func TestExposeServiceRequest_Validate_HTTPAllowsAuth(t *testing.T) {
req := ExposeServiceRequest{Port: 8080, Mode: "http", Pin: "123456"}
require.NoError(t, req.Validate())
}
func TestValidate_HeaderAuths(t *testing.T) {
t.Run("single valid header", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "X-API-Key", Value: "secret"},
},
}
require.NoError(t, rp.Validate())
})
t.Run("multiple headers same canonical name allowed", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "Authorization", Value: "Bearer token-1"},
{Enabled: true, Header: "Authorization", Value: "Bearer token-2"},
},
}
require.NoError(t, rp.Validate())
})
t.Run("multiple headers different case same canonical allowed", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "x-api-key", Value: "key-1"},
{Enabled: true, Header: "X-Api-Key", Value: "key-2"},
},
}
require.NoError(t, rp.Validate())
})
t.Run("multiple different headers allowed", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "Authorization", Value: "Bearer tok"},
{Enabled: true, Header: "X-API-Key", Value: "key"},
},
}
require.NoError(t, rp.Validate())
})
t.Run("empty header name rejected", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "", Value: "val"},
},
}
err := rp.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "header name is required")
})
t.Run("hop-by-hop header rejected", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "Connection", Value: "val"},
},
}
err := rp.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "hop-by-hop")
})
t.Run("host header rejected", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "Host", Value: "val"},
},
}
err := rp.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "Host header cannot be used")
})
t.Run("disabled entries skipped", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: false, Header: "", Value: ""},
{Enabled: true, Header: "X-Key", Value: "val"},
},
}
require.NoError(t, rp.Validate())
})
t.Run("value too long rejected", func(t *testing.T) {
rp := validProxy()
rp.Auth = AuthConfig{
HeaderAuths: []*HeaderAuthConfig{
{Enabled: true, Header: "X-Key", Value: strings.Repeat("a", maxHeaderValueLen+1)},
},
}
err := rp.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum length")
})
}

View File

@@ -197,6 +197,7 @@ func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetr
case "jumpcloud":
return NewJumpCloudManager(JumpCloudClientConfig{
APIToken: config.ExtraConfig["ApiToken"],
ApiUrl: config.ExtraConfig["ApiUrl"],
}, appMetrics)
case "pocketid":
return NewPocketIdManager(PocketIdClientConfig{

View File

@@ -1,24 +1,40 @@
package idp
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
v1 "github.com/TheJumpCloud/jcapi-go/v1"
"github.com/netbirdio/netbird/management/server/telemetry"
)
const (
contentType = "application/json"
accept = "application/json"
jumpCloudDefaultApiUrl = "https://console.jumpcloud.com"
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.
type JumpCloudManager struct {
client *v1.APIClient
apiBase string
apiToken string
httpClient ManagerHTTPClient
credentials ManagerCredentials
@@ -29,6 +45,7 @@ type JumpCloudManager struct {
// JumpCloudClientConfig JumpCloud manager client configurations.
type JumpCloudClientConfig struct {
APIToken string
ApiUrl string
}
// 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")
}
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{
clientConfig: config,
httpClient: httpClient,
@@ -64,7 +89,7 @@ func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppM
}
return &JumpCloudManager{
client: client,
apiBase: apiBase,
apiToken: config.APIToken,
httpClient: httpClient,
credentials: credentials,
@@ -78,37 +103,58 @@ func (jc *JumpCloudCredentials) Authenticate(_ context.Context) (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(_ 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)
// doRequest executes an HTTP request against the JumpCloud V1 API.
func (jm *JumpCloudManager) doRequest(ctx context.Context, method, path string, body io.Reader) ([]byte, error) {
reqURL := jm.apiBase + path
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
if err != nil {
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()
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)
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 {
jm.appMetrics.IDPMetrics().CountGetUserDataByID()
}
var user jumpCloudUser
if err = jm.helper.Unmarshal(body, &user); err != nil {
return nil, err
}
userData := parseJumpCloudUser(user)
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.
func (jm *JumpCloudManager) GetAccount(_ context.Context, accountID string) ([]*UserData, error) {
authCtx := jm.authenticationContext()
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
func (jm *JumpCloudManager) GetAccount(ctx context.Context, accountID string) ([]*UserData, error) {
allUsers, err := jm.searchAllUsers(ctx)
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 {
users := make([]*UserData, 0, len(allUsers))
for _, user := range allUsers {
userData := parseJumpCloudUser(user)
userData.AppMetadata.WTAccountID = accountID
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.
// It returns a list of users indexed by accountID.
func (jm *JumpCloudManager) GetAllAccounts(_ context.Context) (map[string][]*UserData, error) {
authCtx := jm.authenticationContext()
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
func (jm *JumpCloudManager) GetAllAccounts(ctx context.Context) (map[string][]*UserData, error) {
allUsers, err := jm.searchAllUsers(ctx)
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 {
for _, user := range allUsers {
userData := parseJumpCloudUser(user)
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
}
@@ -176,6 +203,41 @@ func (jm *JumpCloudManager) GetAllAccounts(_ context.Context) (map[string][]*Use
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.
func (jm *JumpCloudManager) CreateUser(_ context.Context, _, _, _, _ string) (*UserData, error) {
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.
// 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{}{
"filter": []string{email},
@@ -191,25 +253,26 @@ func (jm *JumpCloudManager) GetUserByEmail(_ context.Context, email string) ([]*
},
}
authCtx := jm.authenticationContext()
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, searchFilter)
payload, err := json.Marshal(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)
body, err := jm.doRequest(ctx, http.MethodPost, "/search/systemusers", bytes.NewReader(payload))
if err != nil {
return nil, err
}
if jm.appMetrics != nil {
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 {
usersData = append(usersData, parseJumpCloudUser(user))
}
@@ -224,20 +287,11 @@ func (jm *JumpCloudManager) InviteUserByID(_ context.Context, _ string) error {
}
// DeleteUser from jumpCloud directory
func (jm *JumpCloudManager) DeleteUser(_ context.Context, userID string) error {
authCtx := jm.authenticationContext()
_, resp, err := jm.client.SystemusersApi.SystemusersDelete(authCtx, userID, contentType, accept, nil)
func (jm *JumpCloudManager) DeleteUser(ctx context.Context, userID string) error {
_, err := jm.doRequest(ctx, http.MethodDelete, "/systemusers/"+userID, 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()
@@ -247,11 +301,11 @@ func (jm *JumpCloudManager) DeleteUser(_ context.Context, userID string) error {
}
// 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}
return &UserData{
Email: user.Email,
Name: strings.Join(names, " "),
ID: user.Id,
ID: user.ID,
}
}

View File

@@ -1,8 +1,15 @@
package idp
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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,
}
}

View File

@@ -249,7 +249,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
if err != nil {
newLabel = ""
} else {
_, err := transaction.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, update.Name)
_, err := transaction.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, newLabel)
if err == nil {
newLabel = ""
}

View File

@@ -37,6 +37,7 @@ import (
"github.com/netbirdio/netbird/management/server/job"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/shared/auth"
"github.com/netbirdio/netbird/shared/management/status"
"github.com/netbirdio/netbird/management/server/util"
@@ -2738,3 +2739,70 @@ func TestProcessPeerAddAuth(t *testing.T) {
assert.Empty(t, config.GroupsToAdd)
})
}
func TestUpdatePeer_DnsLabelCollisionWithFQDN(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err, "unable to create account manager")
accountID, err := manager.GetAccountIDByUserID(context.Background(), auth.UserAuth{UserId: userID})
require.NoError(t, err, "unable to create an account")
// Add first peer with hostname that produces DNS label "netbird1"
key1, err := wgtypes.GenerateKey()
require.NoError(t, err)
peer1, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
Key: key1.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: "netbird1.netbird.cloud"},
}, false)
require.NoError(t, err, "unable to add first peer")
assert.Equal(t, "netbird1", peer1.DNSLabel)
// Add second peer with a different hostname
key2, err := wgtypes.GenerateKey()
require.NoError(t, err)
peer2, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
Key: key2.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: "ip-10-29-5-130"},
}, false)
require.NoError(t, err)
update := peer2.Copy()
update.Name = "netbird1.demo.netbird.cloud"
updated, err := manager.UpdatePeer(context.Background(), accountID, userID, update)
require.NoError(t, err, "renaming peer should not fail with duplicate DNS label error")
assert.Equal(t, "netbird1.demo.netbird.cloud", updated.Name)
assert.NotEqual(t, "netbird1", updated.DNSLabel, "DNS label should not collide with existing peer")
assert.Contains(t, updated.DNSLabel, "netbird1-", "DNS label should be IP-based fallback")
}
func TestUpdatePeer_DnsLabelUniqueName(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err, "unable to create account manager")
accountID, err := manager.GetAccountIDByUserID(context.Background(), auth.UserAuth{UserId: userID})
require.NoError(t, err, "unable to create an account")
key1, err := wgtypes.GenerateKey()
require.NoError(t, err)
peer1, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
Key: key1.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: "web-server"},
}, false)
require.NoError(t, err)
assert.Equal(t, "web-server", peer1.DNSLabel)
// Add second peer and rename it to a unique FQDN whose first label doesn't collide
key2, err := wgtypes.GenerateKey()
require.NoError(t, err)
peer2, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
Key: key2.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: "old-name"},
}, false)
require.NoError(t, err)
update := peer2.Copy()
update.Name = "api-server.example.com"
updated, err := manager.UpdatePeer(context.Background(), accountID, userID, update)
require.NoError(t, err, "renaming to unique FQDN should succeed")
assert.Equal(t, "api-server", updated.DNSLabel, "DNS label should be first label of FQDN")
}

View File

@@ -932,3 +932,71 @@ func TestProtect_HeaderAuth_SubsequentRequestUsesSessionCookie(t *testing.T) {
assert.Equal(t, "header-user", capturedData2.GetUserID())
assert.Equal(t, "header", capturedData2.GetAuthMethod())
}
// TestProtect_HeaderAuth_MultipleValuesSameHeader verifies that the proxy
// correctly handles multiple valid credentials for the same header name.
// In production, the mgmt gRPC authenticateHeader iterates all configured
// header auths and accepts if any hash matches (OR semantics). The proxy
// creates one Header scheme per entry, but a single gRPC call checks all.
func TestProtect_HeaderAuth_MultipleValuesSameHeader(t *testing.T) {
mw := NewMiddleware(log.StandardLogger(), nil, nil)
kp := generateTestKeyPair(t)
// Mock simulates mgmt behavior: accepts either token-a or token-b.
accepted := map[string]bool{"Bearer token-a": true, "Bearer token-b": true}
mock := &mockAuthenticator{fn: func(_ context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateResponse, error) {
ha := req.GetHeaderAuth()
if ha != nil && accepted[ha.GetHeaderValue()] {
token, err := sessionkey.SignToken(kp.PrivateKey, "header-user", "example.com", auth.MethodHeader, time.Hour)
require.NoError(t, err)
return &proto.AuthenticateResponse{Success: true, SessionToken: token}, nil
}
return &proto.AuthenticateResponse{Success: false}, nil
}}
// Single Header scheme (as if one entry existed), but the mock checks both values.
hdr := NewHeader(mock, "svc1", "acc1", "Authorization")
require.NoError(t, mw.AddDomain("example.com", []Scheme{hdr}, kp.PublicKey, time.Hour, "acc1", "svc1", nil))
var backendCalled bool
handler := mw.Protect(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
backendCalled = true
w.WriteHeader(http.StatusOK)
}))
t.Run("first value accepted", func(t *testing.T) {
backendCalled = false
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header.Set("Authorization", "Bearer token-a")
req = req.WithContext(proxy.WithCapturedData(req.Context(), proxy.NewCapturedData("")))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.True(t, backendCalled, "first token should be accepted")
})
t.Run("second value accepted", func(t *testing.T) {
backendCalled = false
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header.Set("Authorization", "Bearer token-b")
req = req.WithContext(proxy.WithCapturedData(req.Context(), proxy.NewCapturedData("")))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.True(t, backendCalled, "second token should be accepted")
})
t.Run("unknown value rejected", func(t *testing.T) {
backendCalled = false
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header.Set("Authorization", "Bearer token-c")
req = req.WithContext(proxy.WithCapturedData(req.Context(), proxy.NewCapturedData("")))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
assert.False(t, backendCalled, "unknown token should be rejected")
})
}

View File

@@ -0,0 +1,112 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"github.com/netbirdio/netbird/shared/management/http/api"
)
// AzureIDPAPI APIs for Azure AD IDP integrations
type AzureIDPAPI struct {
c *Client
}
// List retrieves all Azure AD IDP integrations
func (a *AzureIDPAPI) List(ctx context.Context) ([]api.AzureIntegration, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/azure-idp", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.AzureIntegration](resp)
return ret, err
}
// Get retrieves a specific Azure AD IDP integration by ID
func (a *AzureIDPAPI) Get(ctx context.Context, integrationID string) (*api.AzureIntegration, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/azure-idp/"+integrationID, nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.AzureIntegration](resp)
return &ret, err
}
// Create creates a new Azure AD IDP integration
func (a *AzureIDPAPI) Create(ctx context.Context, request api.CreateAzureIntegrationRequest) (*api.AzureIntegration, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/azure-idp", bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.AzureIntegration](resp)
return &ret, err
}
// Update updates an existing Azure AD IDP integration
func (a *AzureIDPAPI) Update(ctx context.Context, integrationID string, request api.UpdateAzureIntegrationRequest) (*api.AzureIntegration, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/azure-idp/"+integrationID, bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.AzureIntegration](resp)
return &ret, err
}
// Delete deletes an Azure AD IDP integration
func (a *AzureIDPAPI) Delete(ctx context.Context, integrationID string) error {
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/azure-idp/"+integrationID, nil, nil)
if err != nil {
return err
}
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
// Sync triggers a manual sync for an Azure AD IDP integration
func (a *AzureIDPAPI) Sync(ctx context.Context, integrationID string) (*api.SyncResult, error) {
resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/azure-idp/"+integrationID+"/sync", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.SyncResult](resp)
return &ret, err
}
// GetLogs retrieves synchronization logs for an Azure AD IDP integration
func (a *AzureIDPAPI) GetLogs(ctx context.Context, integrationID string) ([]api.IdpIntegrationSyncLog, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/azure-idp/"+integrationID+"/logs", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.IdpIntegrationSyncLog](resp)
return ret, err
}

View File

@@ -0,0 +1,252 @@
//go:build integration
package rest_test
import (
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/http/util"
)
var testAzureIntegration = api.AzureIntegration{
Id: 1,
Enabled: true,
ClientId: "12345678-1234-1234-1234-123456789012",
TenantId: "87654321-4321-4321-4321-210987654321",
SyncInterval: 300,
GroupPrefixes: []string{"eng-"},
UserGroupPrefixes: []string{"dev-"},
Host: "microsoft.com",
LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
func TestAzureIDP_List_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal([]api.AzureIntegration{testAzureIntegration})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.List(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testAzureIntegration, ret[0])
})
}
func TestAzureIDP_List_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.List(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}
func TestAzureIDP_Get_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal(testAzureIntegration)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Get(context.Background(), "int-1")
require.NoError(t, err)
assert.Equal(t, testAzureIntegration, *ret)
})
}
func TestAzureIDP_Get_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Get(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Nil(t, ret)
})
}
func TestAzureIDP_Create_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.CreateAzureIntegrationRequest
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, "12345678-1234-1234-1234-123456789012", req.ClientId)
retBytes, _ := json.Marshal(testAzureIntegration)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Create(context.Background(), api.CreateAzureIntegrationRequest{
ClientId: "12345678-1234-1234-1234-123456789012",
ClientSecret: "secret",
TenantId: "87654321-4321-4321-4321-210987654321",
Host: api.CreateAzureIntegrationRequestHostMicrosoftCom,
GroupPrefixes: &[]string{"eng-"},
})
require.NoError(t, err)
assert.Equal(t, testAzureIntegration, *ret)
})
}
func TestAzureIDP_Create_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Create(context.Background(), api.CreateAzureIntegrationRequest{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestAzureIDP_Update_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.UpdateAzureIntegrationRequest
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, true, *req.Enabled)
retBytes, _ := json.Marshal(testAzureIntegration)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Update(context.Background(), "int-1", api.UpdateAzureIntegrationRequest{
Enabled: ptr(true),
})
require.NoError(t, err)
assert.Equal(t, testAzureIntegration, *ret)
})
}
func TestAzureIDP_Update_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Update(context.Background(), "int-1", api.UpdateAzureIntegrationRequest{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestAzureIDP_Delete_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(200)
})
err := c.AzureIDP.Delete(context.Background(), "int-1")
require.NoError(t, err)
})
}
func TestAzureIDP_Delete_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
err := c.AzureIDP.Delete(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
})
}
func TestAzureIDP_Sync_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1/sync", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
retBytes, _ := json.Marshal(api.SyncResult{Result: ptr("ok")})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Sync(context.Background(), "int-1")
require.NoError(t, err)
assert.Equal(t, "ok", *ret.Result)
})
}
func TestAzureIDP_Sync_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1/sync", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.Sync(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Nil(t, ret)
})
}
func TestAzureIDP_GetLogs_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal([]api.IdpIntegrationSyncLog{testSyncLog})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.GetLogs(context.Background(), "int-1")
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testSyncLog, ret[0])
})
}
func TestAzureIDP_GetLogs_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/azure-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AzureIDP.GetLogs(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Empty(t, ret)
})
}

View File

@@ -110,6 +110,15 @@ type Client struct {
// see more: https://docs.netbird.io/api/resources/scim
SCIM *SCIMAPI
// GoogleIDP NetBird Google Workspace IDP integration APIs
GoogleIDP *GoogleIDPAPI
// AzureIDP NetBird Azure AD IDP integration APIs
AzureIDP *AzureIDPAPI
// OktaScimIDP NetBird Okta SCIM IDP integration APIs
OktaScimIDP *OktaScimIDPAPI
// EventStreaming NetBird Event Streaming integration APIs
// see more: https://docs.netbird.io/api/resources/event-streaming
EventStreaming *EventStreamingAPI
@@ -185,6 +194,9 @@ func (c *Client) initialize() {
c.MSP = &MSPAPI{c}
c.EDR = &EDRAPI{c}
c.SCIM = &SCIMAPI{c}
c.GoogleIDP = &GoogleIDPAPI{c}
c.AzureIDP = &AzureIDPAPI{c}
c.OktaScimIDP = &OktaScimIDPAPI{c}
c.EventStreaming = &EventStreamingAPI{c}
c.IdentityProviders = &IdentityProvidersAPI{c}
c.Ingress = &IngressAPI{c}

View File

@@ -0,0 +1,112 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"github.com/netbirdio/netbird/shared/management/http/api"
)
// GoogleIDPAPI APIs for Google Workspace IDP integrations
type GoogleIDPAPI struct {
c *Client
}
// List retrieves all Google Workspace IDP integrations
func (a *GoogleIDPAPI) List(ctx context.Context) ([]api.GoogleIntegration, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/google-idp", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.GoogleIntegration](resp)
return ret, err
}
// Get retrieves a specific Google Workspace IDP integration by ID
func (a *GoogleIDPAPI) Get(ctx context.Context, integrationID string) (*api.GoogleIntegration, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/google-idp/"+integrationID, nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.GoogleIntegration](resp)
return &ret, err
}
// Create creates a new Google Workspace IDP integration
func (a *GoogleIDPAPI) Create(ctx context.Context, request api.CreateGoogleIntegrationRequest) (*api.GoogleIntegration, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/google-idp", bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.GoogleIntegration](resp)
return &ret, err
}
// Update updates an existing Google Workspace IDP integration
func (a *GoogleIDPAPI) Update(ctx context.Context, integrationID string, request api.UpdateGoogleIntegrationRequest) (*api.GoogleIntegration, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/google-idp/"+integrationID, bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.GoogleIntegration](resp)
return &ret, err
}
// Delete deletes a Google Workspace IDP integration
func (a *GoogleIDPAPI) Delete(ctx context.Context, integrationID string) error {
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/google-idp/"+integrationID, nil, nil)
if err != nil {
return err
}
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
// Sync triggers a manual sync for a Google Workspace IDP integration
func (a *GoogleIDPAPI) Sync(ctx context.Context, integrationID string) (*api.SyncResult, error) {
resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/google-idp/"+integrationID+"/sync", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.SyncResult](resp)
return &ret, err
}
// GetLogs retrieves synchronization logs for a Google Workspace IDP integration
func (a *GoogleIDPAPI) GetLogs(ctx context.Context, integrationID string) ([]api.IdpIntegrationSyncLog, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/google-idp/"+integrationID+"/logs", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.IdpIntegrationSyncLog](resp)
return ret, err
}

View File

@@ -0,0 +1,248 @@
//go:build integration
package rest_test
import (
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/http/util"
)
var testGoogleIntegration = api.GoogleIntegration{
Id: 1,
Enabled: true,
CustomerId: "C01234567",
SyncInterval: 300,
GroupPrefixes: []string{"eng-"},
UserGroupPrefixes: []string{"dev-"},
LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
func TestGoogleIDP_List_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal([]api.GoogleIntegration{testGoogleIntegration})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.List(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testGoogleIntegration, ret[0])
})
}
func TestGoogleIDP_List_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.List(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}
func TestGoogleIDP_Get_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal(testGoogleIntegration)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Get(context.Background(), "int-1")
require.NoError(t, err)
assert.Equal(t, testGoogleIntegration, *ret)
})
}
func TestGoogleIDP_Get_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Get(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Nil(t, ret)
})
}
func TestGoogleIDP_Create_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.CreateGoogleIntegrationRequest
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, "C01234567", req.CustomerId)
retBytes, _ := json.Marshal(testGoogleIntegration)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Create(context.Background(), api.CreateGoogleIntegrationRequest{
CustomerId: "C01234567",
ServiceAccountKey: "key-data",
GroupPrefixes: &[]string{"eng-"},
})
require.NoError(t, err)
assert.Equal(t, testGoogleIntegration, *ret)
})
}
func TestGoogleIDP_Create_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Create(context.Background(), api.CreateGoogleIntegrationRequest{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestGoogleIDP_Update_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.UpdateGoogleIntegrationRequest
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, true, *req.Enabled)
retBytes, _ := json.Marshal(testGoogleIntegration)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Update(context.Background(), "int-1", api.UpdateGoogleIntegrationRequest{
Enabled: ptr(true),
})
require.NoError(t, err)
assert.Equal(t, testGoogleIntegration, *ret)
})
}
func TestGoogleIDP_Update_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Update(context.Background(), "int-1", api.UpdateGoogleIntegrationRequest{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestGoogleIDP_Delete_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(200)
})
err := c.GoogleIDP.Delete(context.Background(), "int-1")
require.NoError(t, err)
})
}
func TestGoogleIDP_Delete_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
err := c.GoogleIDP.Delete(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
})
}
func TestGoogleIDP_Sync_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1/sync", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
retBytes, _ := json.Marshal(api.SyncResult{Result: ptr("ok")})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Sync(context.Background(), "int-1")
require.NoError(t, err)
assert.Equal(t, "ok", *ret.Result)
})
}
func TestGoogleIDP_Sync_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1/sync", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.Sync(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Nil(t, ret)
})
}
func TestGoogleIDP_GetLogs_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal([]api.IdpIntegrationSyncLog{testSyncLog})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.GetLogs(context.Background(), "int-1")
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testSyncLog, ret[0])
})
}
func TestGoogleIDP_GetLogs_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/google-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.GoogleIDP.GetLogs(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Empty(t, ret)
})
}

View File

@@ -0,0 +1,112 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"github.com/netbirdio/netbird/shared/management/http/api"
)
// OktaScimIDPAPI APIs for Okta SCIM IDP integrations
type OktaScimIDPAPI struct {
c *Client
}
// List retrieves all Okta SCIM IDP integrations
func (a *OktaScimIDPAPI) List(ctx context.Context) ([]api.OktaScimIntegration, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/okta-scim-idp", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.OktaScimIntegration](resp)
return ret, err
}
// Get retrieves a specific Okta SCIM IDP integration by ID
func (a *OktaScimIDPAPI) Get(ctx context.Context, integrationID string) (*api.OktaScimIntegration, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/okta-scim-idp/"+integrationID, nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.OktaScimIntegration](resp)
return &ret, err
}
// Create creates a new Okta SCIM IDP integration
func (a *OktaScimIDPAPI) Create(ctx context.Context, request api.CreateOktaScimIntegrationRequest) (*api.OktaScimIntegration, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/okta-scim-idp", bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.OktaScimIntegration](resp)
return &ret, err
}
// Update updates an existing Okta SCIM IDP integration
func (a *OktaScimIDPAPI) Update(ctx context.Context, integrationID string, request api.UpdateOktaScimIntegrationRequest) (*api.OktaScimIntegration, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "PUT", "/api/integrations/okta-scim-idp/"+integrationID, bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.OktaScimIntegration](resp)
return &ret, err
}
// Delete deletes an Okta SCIM IDP integration
func (a *OktaScimIDPAPI) Delete(ctx context.Context, integrationID string) error {
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/integrations/okta-scim-idp/"+integrationID, nil, nil)
if err != nil {
return err
}
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
// RegenerateToken regenerates the SCIM API token for an Okta SCIM integration
func (a *OktaScimIDPAPI) RegenerateToken(ctx context.Context, integrationID string) (*api.ScimTokenResponse, error) {
resp, err := a.c.NewRequest(ctx, "POST", "/api/integrations/okta-scim-idp/"+integrationID+"/token", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.ScimTokenResponse](resp)
return &ret, err
}
// GetLogs retrieves synchronization logs for an Okta SCIM IDP integration
func (a *OktaScimIDPAPI) GetLogs(ctx context.Context, integrationID string) ([]api.IdpIntegrationSyncLog, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/okta-scim-idp/"+integrationID+"/logs", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.IdpIntegrationSyncLog](resp)
return ret, err
}

View File

@@ -0,0 +1,246 @@
//go:build integration
package rest_test
import (
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/http/util"
)
var testOktaScimIntegration = api.OktaScimIntegration{
Id: 1,
AuthToken: "****",
Enabled: true,
GroupPrefixes: []string{"eng-"},
UserGroupPrefixes: []string{"dev-"},
LastSyncedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
func TestOktaScimIDP_List_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal([]api.OktaScimIntegration{testOktaScimIntegration})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.List(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testOktaScimIntegration, ret[0])
})
}
func TestOktaScimIDP_List_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.List(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}
func TestOktaScimIDP_Get_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal(testOktaScimIntegration)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.Get(context.Background(), "int-1")
require.NoError(t, err)
assert.Equal(t, testOktaScimIntegration, *ret)
})
}
func TestOktaScimIDP_Get_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.Get(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Nil(t, ret)
})
}
func TestOktaScimIDP_Create_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.CreateOktaScimIntegrationRequest
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, "my-okta-connection", req.ConnectionName)
retBytes, _ := json.Marshal(testOktaScimIntegration)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.Create(context.Background(), api.CreateOktaScimIntegrationRequest{
ConnectionName: "my-okta-connection",
GroupPrefixes: &[]string{"eng-"},
})
require.NoError(t, err)
assert.Equal(t, testOktaScimIntegration, *ret)
})
}
func TestOktaScimIDP_Create_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.Create(context.Background(), api.CreateOktaScimIntegrationRequest{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestOktaScimIDP_Update_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.UpdateOktaScimIntegrationRequest
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, true, *req.Enabled)
retBytes, _ := json.Marshal(testOktaScimIntegration)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.Update(context.Background(), "int-1", api.UpdateOktaScimIntegrationRequest{
Enabled: ptr(true),
})
require.NoError(t, err)
assert.Equal(t, testOktaScimIntegration, *ret)
})
}
func TestOktaScimIDP_Update_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.Update(context.Background(), "int-1", api.UpdateOktaScimIntegrationRequest{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestOktaScimIDP_Delete_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(200)
})
err := c.OktaScimIDP.Delete(context.Background(), "int-1")
require.NoError(t, err)
})
}
func TestOktaScimIDP_Delete_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
err := c.OktaScimIDP.Delete(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
})
}
func TestOktaScimIDP_RegenerateToken_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1/token", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
retBytes, _ := json.Marshal(testScimToken)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.RegenerateToken(context.Background(), "int-1")
require.NoError(t, err)
assert.Equal(t, testScimToken, *ret)
})
}
func TestOktaScimIDP_RegenerateToken_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1/token", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.RegenerateToken(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Nil(t, ret)
})
}
func TestOktaScimIDP_GetLogs_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal([]api.IdpIntegrationSyncLog{testSyncLog})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.GetLogs(context.Background(), "int-1")
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testSyncLog, ret[0])
})
}
func TestOktaScimIDP_GetLogs_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/okta-scim-idp/int-1/logs", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.OktaScimIDP.GetLogs(context.Background(), "int-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Empty(t, ret)
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,24 @@ const (
TokenAuthScopes = "TokenAuth.Scopes"
)
// Defines values for CreateAzureIntegrationRequestHost.
const (
CreateAzureIntegrationRequestHostMicrosoftCom CreateAzureIntegrationRequestHost = "microsoft.com"
CreateAzureIntegrationRequestHostMicrosoftUs CreateAzureIntegrationRequestHost = "microsoft.us"
)
// Valid indicates whether the value is a known member of the CreateAzureIntegrationRequestHost enum.
func (e CreateAzureIntegrationRequestHost) Valid() bool {
switch e {
case CreateAzureIntegrationRequestHostMicrosoftCom:
return true
case CreateAzureIntegrationRequestHostMicrosoftUs:
return true
default:
return false
}
}
// Defines values for CreateIntegrationRequestPlatform.
const (
CreateIntegrationRequestPlatformDatadog CreateIntegrationRequestPlatform = "datadog"
@@ -1450,6 +1468,36 @@ type AvailablePorts struct {
Udp int `json:"udp"`
}
// AzureIntegration defines model for AzureIntegration.
type AzureIntegration struct {
// ClientId Azure AD application (client) ID
ClientId string `json:"client_id"`
// Enabled Whether the integration is enabled
Enabled bool `json:"enabled"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes []string `json:"group_prefixes"`
// Host Azure host domain for the Graph API
Host string `json:"host"`
// Id The unique identifier for the integration
Id int64 `json:"id"`
// LastSyncedAt Timestamp of the last synchronization
LastSyncedAt time.Time `json:"last_synced_at"`
// SyncInterval Sync interval in seconds
SyncInterval int `json:"sync_interval"`
// TenantId Azure AD tenant ID
TenantId string `json:"tenant_id"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes []string `json:"user_group_prefixes"`
}
// BearerAuthConfig defines model for BearerAuthConfig.
type BearerAuthConfig struct {
// DistributionGroups List of group IDs that can use bearer auth
@@ -1557,6 +1605,51 @@ type Country struct {
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
type CountryCode = string
// CreateAzureIntegrationRequest defines model for CreateAzureIntegrationRequest.
type CreateAzureIntegrationRequest struct {
// ClientId Azure AD application (client) ID
ClientId string `json:"client_id"`
// ClientSecret Base64-encoded Azure AD client secret
ClientSecret string `json:"client_secret"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// Host Azure host domain for the Graph API
Host CreateAzureIntegrationRequestHost `json:"host"`
// SyncInterval Sync interval in seconds (minimum 300). Defaults to 300 if not specified.
SyncInterval *int `json:"sync_interval,omitempty"`
// TenantId Azure AD tenant ID
TenantId string `json:"tenant_id"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
// CreateAzureIntegrationRequestHost Azure host domain for the Graph API
type CreateAzureIntegrationRequestHost string
// CreateGoogleIntegrationRequest defines model for CreateGoogleIntegrationRequest.
type CreateGoogleIntegrationRequest struct {
// CustomerId Customer ID from Google Workspace Account Settings
CustomerId string `json:"customer_id"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// ServiceAccountKey Base64-encoded Google service account key
ServiceAccountKey string `json:"service_account_key"`
// SyncInterval Sync interval in seconds (minimum 300). Defaults to 300 if not specified.
SyncInterval *int `json:"sync_interval,omitempty"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
// CreateIntegrationRequest Request payload for creating a new event streaming integration. Also used as the structure for the PUT request body, but not all fields are applicable for updates (see PUT operation description).
type CreateIntegrationRequest struct {
// Config Platform-specific configuration as key-value pairs. For creation, all necessary credentials and settings must be provided. For updates, provide the fields to change or the entire new configuration.
@@ -1572,7 +1665,19 @@ type CreateIntegrationRequest struct {
// CreateIntegrationRequestPlatform The event streaming platform to integrate with (e.g., "datadog", "s3", "firehose"). This field is used for creation. For updates (PUT), this field, if sent, is ignored by the backend.
type CreateIntegrationRequestPlatform string
// CreateScimIntegrationRequest Request payload for creating an SCIM IDP integration
// CreateOktaScimIntegrationRequest defines model for CreateOktaScimIntegrationRequest.
type CreateOktaScimIntegrationRequest struct {
// ConnectionName The Okta enterprise connection name on Auth0
ConnectionName string `json:"connection_name"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
// CreateScimIntegrationRequest defines model for CreateScimIntegrationRequest.
type CreateScimIntegrationRequest struct {
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
@@ -1947,6 +2052,30 @@ type GeoLocationCheckAction string
// GetTenantsResponse defines model for GetTenantsResponse.
type GetTenantsResponse = []TenantResponse
// GoogleIntegration defines model for GoogleIntegration.
type GoogleIntegration struct {
// CustomerId Customer ID from Google Workspace
CustomerId string `json:"customer_id"`
// Enabled Whether the integration is enabled
Enabled bool `json:"enabled"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes []string `json:"group_prefixes"`
// Id The unique identifier for the integration
Id int64 `json:"id"`
// LastSyncedAt Timestamp of the last synchronization
LastSyncedAt time.Time `json:"last_synced_at"`
// SyncInterval Sync interval in seconds
SyncInterval int `json:"sync_interval"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes []string `json:"user_group_prefixes"`
}
// Group defines model for Group.
type Group struct {
// Id Group ID
@@ -2238,6 +2367,12 @@ type InstanceVersionInfo struct {
ManagementUpdateAvailable bool `json:"management_update_available"`
}
// IntegrationEnabled defines model for IntegrationEnabled.
type IntegrationEnabled struct {
// Enabled Whether the integration is enabled
Enabled *bool `json:"enabled,omitempty"`
}
// IntegrationResponse Represents an event streaming integration.
type IntegrationResponse struct {
// AccountId The identifier of the account this integration belongs to.
@@ -2265,6 +2400,15 @@ type IntegrationResponse struct {
// IntegrationResponsePlatform The event streaming platform.
type IntegrationResponsePlatform string
// IntegrationSyncFilters defines model for IntegrationSyncFilters.
type IntegrationSyncFilters struct {
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
// InvoicePDFResponse defines model for InvoicePDFResponse.
type InvoicePDFResponse struct {
// Url URL to redirect the user to invoice.
@@ -2684,6 +2828,27 @@ type OSVersionCheck struct {
Windows *MinKernelVersionCheck `json:"windows,omitempty"`
}
// OktaScimIntegration defines model for OktaScimIntegration.
type OktaScimIntegration struct {
// AuthToken SCIM API token (full on creation/regeneration, masked on retrieval)
AuthToken string `json:"auth_token"`
// Enabled Whether the integration is enabled
Enabled bool `json:"enabled"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes []string `json:"group_prefixes"`
// Id The unique identifier for the integration
Id int64 `json:"id"`
// LastSyncedAt Timestamp of the last synchronization
LastSyncedAt time.Time `json:"last_synced_at"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes []string `json:"user_group_prefixes"`
}
// PINAuthConfig defines model for PINAuthConfig.
type PINAuthConfig struct {
// Enabled Whether PIN auth is enabled
@@ -3533,12 +3698,12 @@ type RulePortRange struct {
Start int `json:"start"`
}
// ScimIntegration Represents a SCIM IDP integration
// ScimIntegration defines model for ScimIntegration.
type ScimIntegration struct {
// AuthToken SCIM API token (full on creation, masked otherwise)
AuthToken string `json:"auth_token"`
// Enabled Indicates whether the integration is enabled
// Enabled Whether the integration is enabled
Enabled bool `json:"enabled"`
// GroupPrefixes List of start_with string patterns for groups to sync
@@ -3550,6 +3715,9 @@ type ScimIntegration struct {
// LastSyncedAt Timestamp of when the integration was last synced
LastSyncedAt time.Time `json:"last_synced_at"`
// Prefix The connection prefix used for the SCIM provider
Prefix string `json:"prefix"`
// Provider Name of the SCIM identity provider
Provider string `json:"provider"`
@@ -3951,6 +4119,11 @@ type Subscription struct {
UpdatedAt time.Time `json:"updated_at"`
}
// SyncResult Response for a manual sync trigger
type SyncResult struct {
Result *string `json:"result,omitempty"`
}
// TenantGroupResponse defines model for TenantGroupResponse.
type TenantGroupResponse struct {
// Id The Group ID
@@ -3996,14 +4169,74 @@ type TenantResponse struct {
// TenantResponseStatus The status of the tenant
type TenantResponseStatus string
// UpdateScimIntegrationRequest Request payload for updating an SCIM IDP integration
type UpdateScimIntegrationRequest struct {
// Enabled Indicates whether the integration is enabled
// UpdateAzureIntegrationRequest defines model for UpdateAzureIntegrationRequest.
type UpdateAzureIntegrationRequest struct {
// ClientId Azure AD application (client) ID
ClientId *string `json:"client_id,omitempty"`
// ClientSecret Base64-encoded Azure AD client secret
ClientSecret *string `json:"client_secret,omitempty"`
// Enabled Whether the integration is enabled
Enabled *bool `json:"enabled,omitempty"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// SyncInterval Sync interval in seconds (minimum 300)
SyncInterval *int `json:"sync_interval,omitempty"`
// TenantId Azure AD tenant ID
TenantId *string `json:"tenant_id,omitempty"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
// UpdateGoogleIntegrationRequest defines model for UpdateGoogleIntegrationRequest.
type UpdateGoogleIntegrationRequest struct {
// CustomerId Customer ID from Google Workspace Account Settings
CustomerId *string `json:"customer_id,omitempty"`
// Enabled Whether the integration is enabled
Enabled *bool `json:"enabled,omitempty"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// ServiceAccountKey Base64-encoded Google service account key
ServiceAccountKey *string `json:"service_account_key,omitempty"`
// SyncInterval Sync interval in seconds (minimum 300)
SyncInterval *int `json:"sync_interval,omitempty"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
// UpdateOktaScimIntegrationRequest defines model for UpdateOktaScimIntegrationRequest.
type UpdateOktaScimIntegrationRequest struct {
// Enabled Whether the integration is enabled
Enabled *bool `json:"enabled,omitempty"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
// UpdateScimIntegrationRequest defines model for UpdateScimIntegrationRequest.
type UpdateScimIntegrationRequest struct {
// Enabled Whether the integration is enabled
Enabled *bool `json:"enabled,omitempty"`
// GroupPrefixes List of start_with string patterns for groups to sync
GroupPrefixes *[]string `json:"group_prefixes,omitempty"`
// Prefix The connection prefix used for the SCIM provider
Prefix *string `json:"prefix,omitempty"`
// UserGroupPrefixes List of start_with string patterns for groups which users to sync
UserGroupPrefixes *[]string `json:"user_group_prefixes,omitempty"`
}
@@ -4513,6 +4746,12 @@ type PostApiIngressPeersJSONRequestBody = IngressPeerCreateRequest
// PutApiIngressPeersIngressPeerIdJSONRequestBody defines body for PutApiIngressPeersIngressPeerId for application/json ContentType.
type PutApiIngressPeersIngressPeerIdJSONRequestBody = IngressPeerUpdateRequest
// CreateAzureIntegrationJSONRequestBody defines body for CreateAzureIntegration for application/json ContentType.
type CreateAzureIntegrationJSONRequestBody = CreateAzureIntegrationRequest
// UpdateAzureIntegrationJSONRequestBody defines body for UpdateAzureIntegration for application/json ContentType.
type UpdateAzureIntegrationJSONRequestBody = UpdateAzureIntegrationRequest
// PostApiIntegrationsBillingAwsMarketplaceActivateJSONRequestBody defines body for PostApiIntegrationsBillingAwsMarketplaceActivate for application/json ContentType.
type PostApiIntegrationsBillingAwsMarketplaceActivateJSONRequestBody PostApiIntegrationsBillingAwsMarketplaceActivateJSONBody
@@ -4549,6 +4788,12 @@ type CreateSentinelOneEDRIntegrationJSONRequestBody = EDRSentinelOneRequest
// UpdateSentinelOneEDRIntegrationJSONRequestBody defines body for UpdateSentinelOneEDRIntegration for application/json ContentType.
type UpdateSentinelOneEDRIntegrationJSONRequestBody = EDRSentinelOneRequest
// CreateGoogleIntegrationJSONRequestBody defines body for CreateGoogleIntegration for application/json ContentType.
type CreateGoogleIntegrationJSONRequestBody = CreateGoogleIntegrationRequest
// UpdateGoogleIntegrationJSONRequestBody defines body for UpdateGoogleIntegration for application/json ContentType.
type UpdateGoogleIntegrationJSONRequestBody = UpdateGoogleIntegrationRequest
// PostApiIntegrationsMspTenantsJSONRequestBody defines body for PostApiIntegrationsMspTenants for application/json ContentType.
type PostApiIntegrationsMspTenantsJSONRequestBody = CreateTenantRequest
@@ -4564,6 +4809,12 @@ type PostApiIntegrationsMspTenantsIdSubscriptionJSONRequestBody PostApiIntegrati
// PostApiIntegrationsMspTenantsIdUnlinkJSONRequestBody defines body for PostApiIntegrationsMspTenantsIdUnlink for application/json ContentType.
type PostApiIntegrationsMspTenantsIdUnlinkJSONRequestBody PostApiIntegrationsMspTenantsIdUnlinkJSONBody
// CreateOktaScimIntegrationJSONRequestBody defines body for CreateOktaScimIntegration for application/json ContentType.
type CreateOktaScimIntegrationJSONRequestBody = CreateOktaScimIntegrationRequest
// UpdateOktaScimIntegrationJSONRequestBody defines body for UpdateOktaScimIntegration for application/json ContentType.
type UpdateOktaScimIntegrationJSONRequestBody = UpdateOktaScimIntegrationRequest
// CreateSCIMIntegrationJSONRequestBody defines body for CreateSCIMIntegration for application/json ContentType.
type CreateSCIMIntegrationJSONRequestBody = CreateScimIntegrationRequest

View File

@@ -5,13 +5,12 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"runtime"
"testing"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
@@ -20,45 +19,55 @@ import (
)
func Test_S3HandlerGetUploadURL(t *testing.T) {
if runtime.GOOS != "linux" && os.Getenv("CI") == "true" {
t.Skip("Skipping test on non-Linux and CI environment due to docker dependency")
}
if runtime.GOOS == "windows" {
t.Skip("Skipping test on Windows due to potential docker dependency")
if runtime.GOOS != "linux" {
t.Skip("Skipping test on non-Linux due to docker dependency")
}
awsEndpoint := "http://127.0.0.1:4566"
awsRegion := "us-east-1"
ctx := context.Background()
containerRequest := testcontainers.ContainerRequest{
Image: "localstack/localstack:s3-latest",
ExposedPorts: []string{"4566:4566/tcp"},
WaitingFor: wait.ForLog("Ready"),
}
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: containerRequest,
Started: true,
ContainerRequest: testcontainers.ContainerRequest{
Image: "minio/minio:RELEASE.2025-04-22T22-12-26Z",
ExposedPorts: []string{"9000/tcp"},
Env: map[string]string{
"MINIO_ROOT_USER": "minioadmin",
"MINIO_ROOT_PASSWORD": "minioadmin",
},
Cmd: []string{"server", "/data"},
WaitingFor: wait.ForHTTP("/minio/health/ready").WithPort("9000"),
},
Started: true,
})
if err != nil {
t.Error(err)
}
defer func(c testcontainers.Container, ctx context.Context) {
require.NoError(t, err)
t.Cleanup(func() {
if err := c.Terminate(ctx); err != nil {
t.Log(err)
}
}(c, ctx)
})
mappedPort, err := c.MappedPort(ctx, "9000")
require.NoError(t, err)
hostIP, err := c.Host(ctx)
require.NoError(t, err)
awsEndpoint := "http://" + hostIP + ":" + mappedPort.Port()
t.Setenv("AWS_REGION", awsRegion)
t.Setenv("AWS_ENDPOINT_URL", awsEndpoint)
t.Setenv("AWS_ACCESS_KEY_ID", "test")
t.Setenv("AWS_SECRET_ACCESS_KEY", "test")
t.Setenv("AWS_ACCESS_KEY_ID", "minioadmin")
t.Setenv("AWS_SECRET_ACCESS_KEY", "minioadmin")
t.Setenv("AWS_CONFIG_FILE", "")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "")
t.Setenv("AWS_PROFILE", "")
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(awsRegion), config.WithBaseEndpoint(awsEndpoint))
if err != nil {
t.Error(err)
}
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(awsRegion),
config.WithBaseEndpoint(awsEndpoint),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("minioadmin", "minioadmin", "")),
)
require.NoError(t, err)
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.UsePathStyle = true
@@ -66,19 +75,16 @@ func Test_S3HandlerGetUploadURL(t *testing.T) {
})
bucketName := "test"
if _, err := client.CreateBucket(ctx, &s3.CreateBucketInput{
_, err = client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucketName,
}); err != nil {
t.Error(err)
}
})
require.NoError(t, err)
list, err := client.ListBuckets(ctx, &s3.ListBucketsInput{})
if err != nil {
t.Error(err)
}
require.NoError(t, err)
assert.Equal(t, len(list.Buckets), 1)
assert.Equal(t, *list.Buckets[0].Name, bucketName)
require.Len(t, list.Buckets, 1)
require.Equal(t, bucketName, *list.Buckets[0].Name)
t.Setenv(bucketVar, bucketName)