mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-08 16:01:29 +02:00
Harden VNC server, IPC, and management plumbing
This commit is contained in:
@@ -465,6 +465,26 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Explicit defence-in-depth gate before any business logic: we already
|
||||
// rely on AddPeer/SavePolicy to enforce the Peers.Create and
|
||||
// Policies.Create permissions, but checking up-front means a future
|
||||
// refactor that bypasses one of those calls can't silently widen the
|
||||
// endpoint's authority.
|
||||
if allowed, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Peers, operations.Create); err != nil {
|
||||
util.WriteError(r.Context(), status.NewPermissionValidationError(err), w)
|
||||
return
|
||||
} else if !allowed {
|
||||
util.WriteError(r.Context(), status.NewPermissionDeniedError(), w)
|
||||
return
|
||||
}
|
||||
if allowed, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Policies, operations.Create); err != nil {
|
||||
util.WriteError(r.Context(), status.NewPermissionValidationError(err), w)
|
||||
return
|
||||
} else if !allowed {
|
||||
util.WriteError(r.Context(), status.NewPermissionDeniedError(), w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PeerTemporaryAccessRequest
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package peers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/shared/auth"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
// TestCreateTemporaryAccess_RejectsCallerWithoutPeersCreate verifies the
|
||||
// defence-in-depth permission gate added to CreateTemporaryAccess: a user
|
||||
// who cannot create peers must be turned away with 403 before any
|
||||
// AccountManager call runs. Previously this endpoint relied entirely on
|
||||
// SavePolicy/AddPeer's internal permission checks; the explicit gate
|
||||
// makes sure a future refactor that bypasses one of those calls can't
|
||||
// silently widen the endpoint's authority.
|
||||
func TestCreateTemporaryAccess_RejectsCallerWithoutPeersCreate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
permMgr := permissions.NewMockManager(ctrl)
|
||||
|
||||
// Caller lacks Peers.Create: handler must short-circuit before any
|
||||
// AccountManager interaction. We deliberately leave accountManager
|
||||
// nil so the test fails loudly if the handler tries to call it.
|
||||
permMgr.EXPECT().
|
||||
ValidateUserPermissions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(modules.Peers), gomock.Eq(operations.Create)).
|
||||
Return(false, nil).
|
||||
Times(1)
|
||||
|
||||
h := &Handler{
|
||||
permissionsManager: permMgr,
|
||||
}
|
||||
|
||||
pubKey := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
body, _ := json.Marshal(api.PeerTemporaryAccessRequest{
|
||||
Name: "temp",
|
||||
Rules: []string{"netbird-vnc"},
|
||||
WgPubKey: pubKey,
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/peers/peer-id/temporary-access", bytes.NewReader(body))
|
||||
req = mux.SetURLVars(req, map[string]string{"peerId": "peer-id"})
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: "regular_user",
|
||||
Domain: "example.com",
|
||||
AccountId: "acct1",
|
||||
})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.CreateTemporaryAccess(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 Forbidden, got %d (body=%s)", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTemporaryAccess_RejectsCallerWithoutPoliciesCreate covers
|
||||
// the second leg of the gate: a user with Peers.Create but not
|
||||
// Policies.Create must still be refused. Catches a misconfiguration
|
||||
// where one permission is granted broadly but the other isn't.
|
||||
func TestCreateTemporaryAccess_RejectsCallerWithoutPoliciesCreate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
permMgr := permissions.NewMockManager(ctrl)
|
||||
|
||||
permMgr.EXPECT().
|
||||
ValidateUserPermissions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(modules.Peers), gomock.Eq(operations.Create)).
|
||||
Return(true, nil).
|
||||
Times(1)
|
||||
permMgr.EXPECT().
|
||||
ValidateUserPermissions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(modules.Policies), gomock.Eq(operations.Create)).
|
||||
Return(false, nil).
|
||||
Times(1)
|
||||
|
||||
h := &Handler{
|
||||
permissionsManager: permMgr,
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(api.PeerTemporaryAccessRequest{
|
||||
Name: "temp",
|
||||
Rules: []string{"netbird-vnc"},
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/peers/peer-id/temporary-access", bytes.NewReader(body))
|
||||
req = mux.SetURLVars(req, map[string]string{"peerId": "peer-id"})
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: "regular_user",
|
||||
Domain: "example.com",
|
||||
AccountId: "acct1",
|
||||
})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.CreateTemporaryAccess(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 Forbidden, got %d (body=%s)", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -78,13 +78,17 @@ func applyResolvedRuleToState(
|
||||
}
|
||||
|
||||
// handleVNCRule collects VNC authorized users and session pubkeys for a VNC
|
||||
// policy rule. Bidirectional rules grant access in both directions.
|
||||
// policy rule. Bidirectional rules grant access in both directions, so a
|
||||
// peer that appears in the rule's sources also needs the SessionPubKey
|
||||
// pushed (otherwise the Noise_IK handshake against that peer would fail
|
||||
// because its authorizer wouldn't know the client's static key).
|
||||
func (cb ruleAuthCallbacks) handleVNCRule(rule *PolicyRule, peerInSources, peerInDestinations bool, state *peerConnResolveState) {
|
||||
if !peerInDestinations && !(rule.Bidirectional && peerInSources) {
|
||||
receivingPeer := peerInDestinations || (rule.Bidirectional && peerInSources)
|
||||
if !receivingPeer {
|
||||
return
|
||||
}
|
||||
cb.collectVNCUsers(rule, state.vncAuthorizedUsers)
|
||||
if peerInDestinations && rule.SessionPubKey != "" && rule.AuthorizedUser != "" {
|
||||
if rule.SessionPubKey != "" && rule.AuthorizedUser != "" {
|
||||
state.vncSessionPubKeys = append(state.vncSessionPubKeys, VNCSessionPubKey{
|
||||
PubKey: rule.SessionPubKey,
|
||||
UserID: rule.AuthorizedUser,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package types
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestHandleVNCRule_BidirectionalDistributesPubkeyToSourcePeer covers the
|
||||
// latent bug where a bidirectional VNC rule used to drop the
|
||||
// SessionPubKey for the peer that appears only in sources, even though
|
||||
// the rule explicitly grants access in both directions. Without the
|
||||
// pubkey, the source peer's Noise_IK authorizer would not recognise the
|
||||
// client's static key and Noise handshakes against it would fail. The
|
||||
// fix in handleVNCRule must distribute the pubkey to either side of a
|
||||
// bidirectional rule.
|
||||
func TestHandleVNCRule_BidirectionalDistributesPubkeyToSourcePeer(t *testing.T) {
|
||||
rule := &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolNetbirdVNC,
|
||||
Bidirectional: true,
|
||||
AuthorizedUser: "user1",
|
||||
SessionPubKey: "pubkey-base64",
|
||||
SessionDisplayName: "Alice",
|
||||
}
|
||||
cb := ruleAuthCallbacks{
|
||||
collectVNCUsers: func(_ *PolicyRule, _ map[string]map[string]struct{}) {},
|
||||
}
|
||||
state := &peerConnResolveState{
|
||||
vncAuthorizedUsers: make(map[string]map[string]struct{}),
|
||||
}
|
||||
|
||||
cb.handleVNCRule(rule, true /*peerInSources*/, false /*peerInDestinations*/, state)
|
||||
|
||||
if len(state.vncSessionPubKeys) != 1 {
|
||||
t.Fatalf("expected 1 session pubkey distributed to source peer of bidirectional rule, got %d", len(state.vncSessionPubKeys))
|
||||
}
|
||||
if state.vncSessionPubKeys[0].PubKey != "pubkey-base64" {
|
||||
t.Fatalf("unexpected pubkey: %q", state.vncSessionPubKeys[0].PubKey)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleVNCRule_UnidirectionalSourceGetsNoPubkey makes sure the fix
|
||||
// above didn't widen pubkey distribution past the bidirectional case:
|
||||
// a strictly source-to-destination rule still must not push the
|
||||
// SessionPubKey to peers that appear only in sources.
|
||||
func TestHandleVNCRule_UnidirectionalSourceGetsNoPubkey(t *testing.T) {
|
||||
rule := &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolNetbirdVNC,
|
||||
Bidirectional: false,
|
||||
AuthorizedUser: "user1",
|
||||
SessionPubKey: "pubkey-base64",
|
||||
}
|
||||
cb := ruleAuthCallbacks{
|
||||
collectVNCUsers: func(_ *PolicyRule, _ map[string]map[string]struct{}) {},
|
||||
}
|
||||
state := &peerConnResolveState{
|
||||
vncAuthorizedUsers: make(map[string]map[string]struct{}),
|
||||
}
|
||||
|
||||
cb.handleVNCRule(rule, true /*peerInSources*/, false /*peerInDestinations*/, state)
|
||||
|
||||
if len(state.vncSessionPubKeys) != 0 {
|
||||
t.Fatalf("expected NO pubkey for source peer of unidirectional rule, got %d", len(state.vncSessionPubKeys))
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleVNCRule_DestinationAlwaysGetsPubkey is the baseline case:
|
||||
// destination peers must always receive the SessionPubKey since they're
|
||||
// the ones that need to authenticate the incoming Noise handshake.
|
||||
func TestHandleVNCRule_DestinationAlwaysGetsPubkey(t *testing.T) {
|
||||
rule := &PolicyRule{
|
||||
Protocol: PolicyRuleProtocolNetbirdVNC,
|
||||
Bidirectional: false,
|
||||
AuthorizedUser: "user1",
|
||||
SessionPubKey: "pubkey-base64",
|
||||
}
|
||||
cb := ruleAuthCallbacks{
|
||||
collectVNCUsers: func(_ *PolicyRule, _ map[string]map[string]struct{}) {},
|
||||
}
|
||||
state := &peerConnResolveState{
|
||||
vncAuthorizedUsers: make(map[string]map[string]struct{}),
|
||||
}
|
||||
|
||||
cb.handleVNCRule(rule, false /*peerInSources*/, true /*peerInDestinations*/, state)
|
||||
|
||||
if len(state.vncSessionPubKeys) != 1 {
|
||||
t.Fatalf("expected 1 session pubkey for destination peer, got %d", len(state.vncSessionPubKeys))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user