mirror of
https://github.com/netbirdio/netbird.git
synced 2025-08-15 17:52:47 +02:00
[management] REST client impersonation (#3879)
This commit is contained in:
@ -86,6 +86,7 @@ func NewWithBearerToken(managementURL, token string) *Client {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewWithOptions initialize new Client instance with options
|
||||||
func NewWithOptions(opts ...option) *Client {
|
func NewWithOptions(opts ...option) *Client {
|
||||||
client := &Client{
|
client := &Client{
|
||||||
httpClient: http.DefaultClient,
|
httpClient: http.DefaultClient,
|
||||||
@ -115,6 +116,7 @@ func (c *Client) initialize() {
|
|||||||
c.Events = &EventsAPI{c}
|
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) {
|
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)
|
req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
48
management/client/rest/impersonation.go
Normal file
48
management/client/rest/impersonation.go
Normal 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)
|
||||||
|
}
|
77
management/client/rest/impersonation_test.go
Normal file
77
management/client/rest/impersonation_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
@ -2,32 +2,41 @@ package rest
|
|||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
|
// option modifier for creation of Client
|
||||||
type option func(*Client)
|
type option func(*Client)
|
||||||
|
|
||||||
|
// HTTPClient interface for HTTP client
|
||||||
type HttpClient interface {
|
type HttpClient interface {
|
||||||
Do(req *http.Request) (*http.Response, error)
|
Do(req *http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHTTPClient overrides HTTPClient used
|
||||||
func WithHttpClient(client HttpClient) option {
|
func WithHttpClient(client HttpClient) option {
|
||||||
return func(c *Client) {
|
return func(c *Client) {
|
||||||
c.httpClient = client
|
c.httpClient = client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBearerToken uses provided bearer token acquired from SSO for authentication
|
||||||
func WithBearerToken(token string) option {
|
func WithBearerToken(token string) option {
|
||||||
return WithAuthHeader("Bearer " + token)
|
return WithAuthHeader("Bearer " + token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPAT uses provided Personal Access Token
|
||||||
|
// (created from NetBird Management Dashboard) for authentication
|
||||||
func WithPAT(token string) option {
|
func WithPAT(token string) option {
|
||||||
return WithAuthHeader("Token " + token)
|
return WithAuthHeader("Token " + token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithManagementURL overrides target NetBird Management server
|
||||||
func WithManagementURL(url string) option {
|
func WithManagementURL(url string) option {
|
||||||
return func(c *Client) {
|
return func(c *Client) {
|
||||||
c.managementURL = url
|
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 {
|
func WithAuthHeader(value string) option {
|
||||||
return func(c *Client) {
|
return func(c *Client) {
|
||||||
c.authHeader = value
|
c.authHeader = value
|
||||||
|
@ -45,7 +45,7 @@ type Settings struct {
|
|||||||
// Extra is a dictionary of Account settings
|
// Extra is a dictionary of Account settings
|
||||||
Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"`
|
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"`
|
LazyConnectionEnabled bool `gorm:"default:false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user