Ctrl K
ComunidadGuías…

tests/firewall_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"net/http"
	"regexp"
	"testing"

	"github.com/shellhub-io/shellhub/pkg/models"
	"github.com/shellhub-io/shellhub/tests/environment"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/ssh"
)

const (
	firewallBlockedLog   = "destination device is blocked by a firewall rule"
	errFirewallRuleBlock = "because a firewall rule block your connection"
	anyValue             = ".*"
)

var anyDevice = environment.FirewallRuleFilter{Hostname: anyValue}

func TestEnterpriseFirewall(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	compose := newEnterpriseEnvironment(t, ctx, environment.New(t, run))
	signer := registerDeviceKey(t, ctx, compose)
	_, device := startAcceptedAgent(t, ctx, compose)

	sshid := deviceSSHID(device)
	source := regexp.QuoteMeta(clientAddress(t, compose, sshid, signer))
	hostname := environment.FirewallRuleFilter{Hostname: regexp.QuoteMeta(device.Name)}
	denyAll := deny(100, matching(anyValue, anyValue, anyDevice))

	createTag(t, compose, "firewalled")
	createTag(t, compose, "elsewhere")
	tagDevice(t, compose, device.UID, "firewalled")

	t.Run("a connection is let through when the namespace has no rules", func(t *testing.T) {
		require.Zero(t, compose.FirewallRuleCount(t))

		requireKeyLogsIn(t, compose, sshid, signer)
	})

	t.Run("an allow rule matching the source address, the user and the hostname lets the connection through", func(t *testing.T) {
		addFirewallRule(t, compose, denyAll)
		addFirewallRule(t, compose, allow(1, matching(source, ShellHubAgentUsername, hostname)))

		requireKeyLogsIn(t, compose, sshid, signer)
	})

	denials := []struct {
		description string
		rule        environment.FirewallRule
	}{
		{description: "source address", rule: matching(source, anyValue, anyDevice)},
		{description: "user", rule: matching(anyValue, ShellHubAgentUsername, anyDevice)},
		{description: "hostname", rule: matching(anyValue, anyValue, hostname)},
		{description: "tag", rule: matching(anyValue, anyValue, environment.FirewallRuleFilter{Tags: []string{"firewalled"}})},
	}

	for _, tc := range denials {
		t.Run("a deny rule matching the "+tc.description+" blocks the connection", func(t *testing.T) {
			addFirewallRule(t, compose, deny(1, tc.rule))

			requireAccessDenied(t, compose, sshid, signer, firewallBlockedLog, errFirewallRuleBlock)
		})
	}

	misses := []struct {
		description string
		rule        environment.FirewallRule
	}{
		{description: "another source address", rule: matching(`192\.0\.2\.1`, anyValue, anyDevice)},
		{description: "another user", rule: matching(anyValue, "nobody", anyDevice)},
		{description: "another hostname", rule: matching(anyValue, anyValue, environment.FirewallRuleFilter{Hostname: "elsewhere"})},
		{description: "a tag the device does not carry", rule: matching(anyValue, anyValue, environment.FirewallRuleFilter{Tags: []string{"elsewhere"}})},
	}

	for _, tc := range misses {
		t.Run("a deny rule for "+tc.description+" lets the connection through", func(t *testing.T) {
			addFirewallRule(t, compose, deny(1, tc.rule))

			requireKeyLogsIn(t, compose, sshid, signer)
		})
	}

	t.Run("an allow rule with a lower priority value wins over a matching deny rule", func(t *testing.T) {
		addFirewallRule(t, compose, denyAll)
		addFirewallRule(t, compose, allow(1, matching(anyValue, anyValue, anyDevice)))

		requireKeyLogsIn(t, compose, sshid, signer)
	})

	t.Run("a deny rule with a lower priority value wins over a matching allow rule", func(t *testing.T) {
		addFirewallRule(t, compose, allow(200, matching(anyValue, anyValue, anyDevice)))
		addFirewallRule(t, compose, denyAll)

		requireAccessDenied(t, compose, sshid, signer, firewallBlockedLog, errFirewallRuleBlock)
	})

	t.Run("an inactive deny rule is skipped", func(t *testing.T) {
		inactive := denyAll
		inactive.Active = false
		addFirewallRule(t, compose, inactive)

		requireKeyLogsIn(t, compose, sshid, signer)
	})

	t.Run("a renamed tag keeps the rule matching the devices carrying it", func(t *testing.T) {
		createTag(t, compose, "rulebefore")
		t.Cleanup(func() { deleteTag(t, compose, "ruleafter") })
		attachTag(t, compose, device.UID, "rulebefore")
		rule := addFirewallRule(t, compose, deny(1, matching(anyValue, anyValue, environment.FirewallRuleFilter{Tags: []string{"rulebefore"}})))

		renameTag(t, compose, "rulebefore", "ruleafter")

		assert.Equal(t, []string{"ruleafter"}, firewallRuleTags(t, compose, rule))
		requireAccessDenied(t, compose, sshid, signer, firewallBlockedLog, errFirewallRuleBlock)
	})

	t.Run("an upgraded namespace is not held to its firewall rules", func(t *testing.T) {
		compose.UpgradeNamespace(t, ShellHubNamespace)

		owner, _ := enrollOwnerKey(t, compose)
		addFirewallRule(t, compose, denyAll)

		requireStraightThrough(t, compose, sshid, owner)
	})
}

func matching(sourceIP, username string, filter environment.FirewallRuleFilter) environment.FirewallRule {
	return environment.FirewallRule{Active: true, SourceIP: sourceIP, Username: username, Filter: filter}
}

func allow(priority int, rule environment.FirewallRule) environment.FirewallRule {
	rule.Priority = priority
	rule.Action = environment.FirewallAllow

	return rule
}

func deny(priority int, rule environment.FirewallRule) environment.FirewallRule {
	rule.Priority = priority
	rule.Action = environment.FirewallDeny

	return rule
}

func addFirewallRule(t *testing.T, compose *environment.DockerCompose, rule environment.FirewallRule) string {
	t.Helper()

	id := compose.CreateFirewallRule(t, rule)
	t.Cleanup(func() { compose.DeleteFirewallRule(t, id) })

	return id
}

func firewallRuleTags(t *testing.T, compose *environment.DockerCompose, id string) []string {
	t.Helper()

	rule := struct {
		Filter struct {
			Tags []models.Tag `json:"tags"`
		} `json:"filter"`
	}{}

	resp, err := compose.R(t.Context()).SetResult(&rule).Get("/api/firewall/rules/" + id)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())

	return tagNames(rule.Filter.Tags)
}

func clientAddress(t *testing.T, compose *environment.DockerCompose, sshid string, signer ssh.Signer) string {
	t.Helper()

	requireKeyLogsIn(t, compose, sshid, signer)

	sessions := []models.Session{}

	resp, err := compose.R(t.Context()).SetResult(&sessions).Get("/api/sessions")
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
	require.NotEmpty(t, sessions)
	require.NotEmpty(t, sessions[0].IPAddress)

	return sessions[0].IPAddress
}