mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 09:01:29 +02:00
* switch to go.uber.org/mock/gomock Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * updated go:generate commands + regenerated mocks Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * update go:generate mockgen commands Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * removed duplicate import Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * fix go:generate Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> --------- Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
169 lines
7.0 KiB
Go
169 lines
7.0 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"go.uber.org/mock/gomock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/codes"
|
|
grpcstatus "google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
)
|
|
|
|
// capturingAuthorizer records the arguments of the last AuthorizeProxyConnect
|
|
// call and returns a fixed error.
|
|
type capturingAuthorizer struct {
|
|
called int
|
|
token *types.ProxyAccessToken
|
|
proxyID string
|
|
address string
|
|
err error
|
|
}
|
|
|
|
func (a *capturingAuthorizer) AuthorizeProxyConnect(_ context.Context, token *types.ProxyAccessToken, proxyID, address string) error {
|
|
a.called++
|
|
a.token = token
|
|
a.proxyID = proxyID
|
|
a.address = address
|
|
return a.err
|
|
}
|
|
|
|
// authorizerServer builds a ProxyServiceServer whose proxy manager reports
|
|
// every cluster address as available, so the authorizer is the only thing
|
|
// standing between a claim and success.
|
|
func authorizerServer(t *testing.T) *ProxyServiceServer {
|
|
t.Helper()
|
|
ctrl := gomock.NewController(t)
|
|
mgr := proxy.NewMockManager(ctrl)
|
|
mgr.EXPECT().IsClusterAddressAvailable(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).AnyTimes()
|
|
return &ProxyServiceServer{proxyManager: mgr}
|
|
}
|
|
|
|
// TestValidateProxyConnect_NilAuthorizerUnchanged guards the no-behavior-change
|
|
// claim: with no authorizer installed — the OSS default — a well-formed claim
|
|
// succeeds and malformed input is rejected exactly as before the hook existed.
|
|
func TestValidateProxyConnect_NilAuthorizerUnchanged(t *testing.T) {
|
|
s := authorizerServer(t)
|
|
|
|
params, err := s.validateProxyConnect("proxy-1", "cluster.example.com", scopedCtx("acc-1"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "proxy-1", params.proxyID)
|
|
assert.Equal(t, "cluster.example.com", params.address)
|
|
|
|
_, err = s.validateProxyConnect("", "cluster.example.com", scopedCtx("acc-1"))
|
|
require.Error(t, err)
|
|
st, ok := grpcstatus.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, codes.InvalidArgument, st.Code(), "missing proxy_id must stay InvalidArgument")
|
|
|
|
_, err = s.validateProxyConnect("proxy-1", "not a hostname", scopedCtx("acc-1"))
|
|
require.Error(t, err)
|
|
st, ok = grpcstatus.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, codes.InvalidArgument, st.Code(), "invalid address must stay InvalidArgument")
|
|
}
|
|
|
|
// TestValidateProxyConnect_AuthorizerReceivesClaim pins the hook contract: the
|
|
// authorizer sees the presented token and the claimed proxy ID and address,
|
|
// and an authorized claim proceeds.
|
|
func TestValidateProxyConnect_AuthorizerReceivesClaim(t *testing.T) {
|
|
s := authorizerServer(t)
|
|
auth := &capturingAuthorizer{}
|
|
s.SetProxyConnectAuthorizer(auth)
|
|
|
|
params, err := s.validateProxyConnect("proxy-1", "cluster.example.com", scopedCtx("acc-1"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "cluster.example.com", params.address)
|
|
|
|
require.Equal(t, 1, auth.called, "authorizer must be consulted exactly once per connect")
|
|
assert.Equal(t, "proxy-1", auth.proxyID)
|
|
assert.Equal(t, "cluster.example.com", auth.address)
|
|
require.NotNil(t, auth.token, "the presented token must be handed to the authorizer")
|
|
require.NotNil(t, auth.token.AccountID)
|
|
assert.Equal(t, "acc-1", *auth.token.AccountID)
|
|
}
|
|
|
|
// TestValidateProxyConnect_PlainErrorBecomesPermissionDenied pins the error
|
|
// mapping: a non-status error from the authorizer surfaces as
|
|
// PermissionDenied — distinguishable from the AlreadyExists used for address
|
|
// conflicts — and the claim does not proceed even though the address itself
|
|
// was available.
|
|
func TestValidateProxyConnect_PlainErrorBecomesPermissionDenied(t *testing.T) {
|
|
s := authorizerServer(t)
|
|
s.SetProxyConnectAuthorizer(&capturingAuthorizer{err: errors.New("not the assigned credential")})
|
|
|
|
_, err := s.validateProxyConnect("proxy-1", "cluster.example.com", scopedCtx("acc-1"))
|
|
require.Error(t, err)
|
|
st, ok := grpcstatus.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, codes.PermissionDenied, st.Code())
|
|
assert.Contains(t, st.Message(), "not the assigned credential", "the authorizer's reason must survive into the status message")
|
|
}
|
|
|
|
// TestValidateProxyConnect_StatusErrorPassesThrough pins that an authorizer
|
|
// which chooses its own status code is not second-guessed.
|
|
func TestValidateProxyConnect_StatusErrorPassesThrough(t *testing.T) {
|
|
s := authorizerServer(t)
|
|
s.SetProxyConnectAuthorizer(&capturingAuthorizer{
|
|
err: grpcstatus.Errorf(codes.ResourceExhausted, "try later"),
|
|
})
|
|
|
|
_, err := s.validateProxyConnect("proxy-1", "cluster.example.com", scopedCtx("acc-1"))
|
|
require.Error(t, err)
|
|
st, ok := grpcstatus.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, codes.ResourceExhausted, st.Code(), "a status error must pass through unchanged")
|
|
assert.Equal(t, "try later", st.Message())
|
|
}
|
|
|
|
// TestValidateProxyConnect_AuthorizerSeesGlobalAndTokenlessConnects pins the
|
|
// call-site placement: the authorizer sits outside the account-scoped branch,
|
|
// so management-wide tokens (AccountID == nil) and connections without any
|
|
// token are also presented to it rather than bypassing policy.
|
|
func TestValidateProxyConnect_AuthorizerSeesGlobalAndTokenlessConnects(t *testing.T) {
|
|
s := &ProxyServiceServer{} // no proxy manager: neither path may reach the availability check
|
|
|
|
auth := &capturingAuthorizer{}
|
|
s.SetProxyConnectAuthorizer(auth)
|
|
|
|
_, err := s.validateProxyConnect("proxy-1", "cluster.example.com", globalCtx())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, auth.called, "a management-wide token must still be presented to the authorizer")
|
|
require.NotNil(t, auth.token)
|
|
assert.Nil(t, auth.token.AccountID)
|
|
|
|
_, err = s.validateProxyConnect("proxy-1", "cluster.example.com", context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, auth.called, "a token-less connect must still be presented to the authorizer")
|
|
assert.Nil(t, auth.token, "no token in context must surface as a nil token, not a zero value")
|
|
}
|
|
|
|
// TestValidateProxyConnect_AuthorizerRunsLast pins the ordering: input
|
|
// validation and the availability check precede policy, so the authorizer is
|
|
// never consulted about a claim that is malformed or already rejected.
|
|
func TestValidateProxyConnect_AuthorizerRunsLast(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
mgr := proxy.NewMockManager(ctrl)
|
|
mgr.EXPECT().IsClusterAddressAvailable(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil)
|
|
s := &ProxyServiceServer{proxyManager: mgr}
|
|
|
|
auth := &capturingAuthorizer{}
|
|
s.SetProxyConnectAuthorizer(auth)
|
|
|
|
_, err := s.validateProxyConnect("proxy-1", "not a hostname", scopedCtx("acc-1"))
|
|
require.Error(t, err)
|
|
assert.Zero(t, auth.called, "a malformed address must be rejected before policy runs")
|
|
|
|
_, err = s.validateProxyConnect("proxy-1", "cluster.example.com", scopedCtx("acc-1"))
|
|
require.Error(t, err)
|
|
st, ok := grpcstatus.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, codes.AlreadyExists, st.Code(), "an address conflict must keep its own status")
|
|
assert.Zero(t, auth.called, "a conflicting address must be rejected before policy runs")
|
|
}
|