diff --git a/management/client/rest/client.go b/management/client/rest/client.go index 25e8ad0da..8bf11caae 100644 --- a/management/client/rest/client.go +++ b/management/client/rest/client.go @@ -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 { diff --git a/management/client/rest/impersonation.go b/management/client/rest/impersonation.go new file mode 100644 index 000000000..4d47c9373 --- /dev/null +++ b/management/client/rest/impersonation.go @@ -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) +} diff --git a/management/client/rest/impersonation_test.go b/management/client/rest/impersonation_test.go new file mode 100644 index 000000000..69c0f9728 --- /dev/null +++ b/management/client/rest/impersonation_test.go @@ -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) + }) +} diff --git a/management/client/rest/options.go b/management/client/rest/options.go index 5aad7dd7e..21f2394e9 100644 --- a/management/client/rest/options.go +++ b/management/client/rest/options.go @@ -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 diff --git a/management/server/types/settings.go b/management/server/types/settings.go index bd361f3ff..a22a36b03 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -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"` }