[management] REST client impersonation (#3879)

This commit is contained in:
Pedro Maia Costa
2025-06-02 21:11:28 +01:00
committed by GitHub
parent 41cd4952f1
commit 07b220d91b
5 changed files with 137 additions and 1 deletions

View File

@ -86,6 +86,7 @@ func NewWithBearerToken(managementURL, token string) *Client {
)
}
// NewWithOptions initialize new Client instance with options
func NewWithOptions(opts ...option) *Client {
client := &Client{
httpClient: http.DefaultClient,
@ -115,6 +116,7 @@ func (c *Client) initialize() {
c.Events = &EventsAPI{c}
}
// NewRequest creates and executes new management API request
func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body)
if err != nil {

View File

@ -0,0 +1,48 @@
package rest
import (
"net/http"
"net/url"
)
// Impersonate returns a Client impersonated for a specific account
func (c *Client) Impersonate(account string) *Client {
client := NewWithOptions(
WithManagementURL(c.managementURL),
WithAuthHeader(c.authHeader),
WithHttpClient(newImpersonatedHttpClient(c, account)),
)
return client
}
type impersonatedHttpClient struct {
baseClient HttpClient
account string
}
func newImpersonatedHttpClient(c *Client, account string) *impersonatedHttpClient {
if hc, ok := c.httpClient.(*impersonatedHttpClient); ok {
hc.account = account
return hc
}
return &impersonatedHttpClient{
baseClient: c.httpClient,
account: account,
}
}
func (c *impersonatedHttpClient) Do(req *http.Request) (*http.Response, error) {
parsedURL, err := url.Parse(req.URL.String())
if err != nil {
return nil, err
}
query := parsedURL.Query()
query.Set("account", c.account)
parsedURL.RawQuery = query.Encode()
req.URL = parsedURL
return c.baseClient.Do(req)
}

View File

@ -0,0 +1,77 @@
//go:build integration
// +build integration
package rest_test
import (
"context"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/client/rest"
"github.com/netbirdio/netbird/management/server/http/api"
)
var (
testImpersonatedAccount = api.Account{
Id: "ImpersonatedTest",
Settings: api.AccountSettings{
Extra: &api.AccountExtraSettings{
PeerApprovalEnabled: false,
},
GroupsPropagationEnabled: ptr(true),
JwtGroupsEnabled: ptr(false),
PeerInactivityExpiration: 7,
PeerInactivityExpirationEnabled: true,
PeerLoginExpiration: 24,
PeerLoginExpirationEnabled: true,
RegularUsersViewBlocked: false,
RoutingPeerDnsResolutionEnabled: ptr(false),
},
}
)
func TestImpersonation_Peers_List_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
impersonatedClient := c.Impersonate(testImpersonatedAccount.Id)
mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, r.URL.Query().Get("account"), testImpersonatedAccount.Id)
retBytes, _ := json.Marshal([]api.Peer{testPeer})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := impersonatedClient.Peers.List(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testPeer, ret[0])
})
}
func TestImpersonation_Change_Account(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
impersonatedClient := c.Impersonate(testImpersonatedAccount.Id)
mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, r.URL.Query().Get("account"), testImpersonatedAccount.Id)
retBytes, _ := json.Marshal([]api.Peer{testPeer})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
_, err := impersonatedClient.Peers.List(context.Background())
require.NoError(t, err)
impersonatedClient = impersonatedClient.Impersonate("another-test-account")
mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, r.URL.Query().Get("account"), "another-test-account")
retBytes, _ := json.Marshal(testPeer)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
_, err = impersonatedClient.Peers.Get(context.Background(), "Test")
require.NoError(t, err)
})
}

View File

@ -2,32 +2,41 @@ package rest
import "net/http"
// option modifier for creation of Client
type option func(*Client)
// HTTPClient interface for HTTP client
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// WithHTTPClient overrides HTTPClient used
func WithHttpClient(client HttpClient) option {
return func(c *Client) {
c.httpClient = client
}
}
// WithBearerToken uses provided bearer token acquired from SSO for authentication
func WithBearerToken(token string) option {
return WithAuthHeader("Bearer " + token)
}
// WithPAT uses provided Personal Access Token
// (created from NetBird Management Dashboard) for authentication
func WithPAT(token string) option {
return WithAuthHeader("Token " + token)
}
// WithManagementURL overrides target NetBird Management server
func WithManagementURL(url string) option {
return func(c *Client) {
c.managementURL = url
}
}
// WithAuthHeader overrides auth header completely, this should generally not be used
// and WithBearerToken or WithPAT should be used instead
func WithAuthHeader(value string) option {
return func(c *Client) {
c.authHeader = value

View File

@ -45,7 +45,7 @@ type Settings struct {
// Extra is a dictionary of Account settings
Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"`
// LazyConnectionEnabled indicates wether the experimental feature is enabled or disabled
// LazyConnectionEnabled indicates if the experimental feature is enabled or disabled
LazyConnectionEnabled bool `gorm:"default:false"`
}