Ctrl K
ComunidadGuías…

tests/access_policy_selector_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/api/authorizer"
	"github.com/shellhub-io/shellhub/pkg/api/requests"
	"github.com/shellhub-io/shellhub/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestAccessPolicySelectorTags covers who may change which devices an access policy selects: a tag
// a policy's filter holds is moved on or off a device only by a role that may edit the policy.
func TestAccessPolicySelectorTags(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	compose := newSSHEnvironment(t, ctx, newNamespace)
	_, device := startAcceptedAgent(t, ctx, compose)

	sshid := deviceSSHID(device)

	t.Run("an operator does not attach a tag an allow policy selects by", func(t *testing.T) {
		member, signer := memberWithKey(t, compose, "attacher", authorizer.RoleOperator)

		createTag(t, compose, "selected")
		t.Cleanup(func() { deleteTag(t, compose, "selected") })

		grant(t, compose, &requests.AccessPolicyCreate{
			Name:    "selected",
			Subject: userSubject(member.ID),
			Filter:  requests.AccessPolicyFilter{Tags: []string{"selected"}},
			Logins:  []string{"*"},
		})

		resp, err := asBearer(t, compose, member.Token).Post("/api/devices/" + device.UID + "/tags/selected")
		require.NoError(t, err)
		t.Cleanup(func() {
			_, _ = compose.R(context.WithoutCancel(t.Context())).Delete("/api/devices/" + device.UID + "/tags/selected")
		})

		assert.Equal(t, http.StatusForbidden, resp.StatusCode(), resp.String())

		mark := compose.ServerLogMark(t)
		requireRefusedAtAuth(t, compose, sshid, signer)
		compose.AwaitServerLogLine(t, mark, "reason="+string(models.ReasonNoGrant), "user="+member.ID)
	})

	t.Run("an operator does not detach a tag a deny policy selects by", func(t *testing.T) {
		member, signer := memberWithKey(t, compose, "detacher", authorizer.RoleOperator)

		createTag(t, compose, "fenced")
		t.Cleanup(func() { deleteTag(t, compose, "fenced") })
		tagDevice(t, compose, device.UID, "fenced")

		grant(t, compose, &requests.AccessPolicyCreate{
			Name:    "operators",
			Subject: roleSubject(authorizer.RoleOperator),
			Filter:  requests.AccessPolicyFilter{All: true},
			Logins:  []string{"*"},
		})
		grant(t, compose, &requests.AccessPolicyCreate{
			Name:    "fenced",
			Subject: userSubject(member.ID),
			Filter:  requests.AccessPolicyFilter{Tags: []string{"fenced"}},
			Logins:  []string{"*"},
			Action:  string(models.PolicyActionDeny),
		})

		resp, err := asBearer(t, compose, member.Token).Delete("/api/devices/" + device.UID + "/tags/fenced")
		require.NoError(t, err)
		t.Cleanup(func() {
			_, _ = compose.R(context.WithoutCancel(t.Context())).Post("/api/devices/" + device.UID + "/tags/fenced")
		})

		assert.Equal(t, http.StatusForbidden, resp.StatusCode(), resp.String())

		mark := compose.ServerLogMark(t)
		requireRefusedAtAuth(t, compose, sshid, signer)
		compose.AwaitServerLogLine(t, mark, "reason="+string(models.ReasonDeniedByPolicy), "user="+member.ID)
	})

	t.Run("an operator attaches and detaches a tag no rule selects by", func(t *testing.T) {
		member := newMember(t, compose, "labeller", authorizer.RoleOperator)

		createTag(t, compose, "label")
		t.Cleanup(func() { deleteTag(t, compose, "label") })

		resp, err := asBearer(t, compose, member.Token).Post("/api/devices/" + device.UID + "/tags/label")
		require.NoError(t, err)
		require.Equal(t, http.StatusNoContent, resp.StatusCode(), resp.String())
		requireDeviceTags(t, compose, device.UID, "label")

		resp, err = asBearer(t, compose, member.Token).Delete("/api/devices/" + device.UID + "/tags/label")
		require.NoError(t, err)
		require.Equal(t, http.StatusNoContent, resp.StatusCode(), resp.String())
		requireDeviceTags(t, compose, device.UID)
	})
}

// TestPublicKeySelectorHostname covers who may rename a device a public key selects by name: a
// rename that moves the device out of the key's pattern needs a role that may edit the key, and
// one between two names the pattern matches does not.
func TestPublicKeySelectorHostname(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	compose := newSSHEnvironment(t, ctx, legacyNamespace)
	_, device := startAcceptedAgent(t, ctx, compose)

	kept := device.Name + "-kept"
	pattern := regexp.QuoteMeta(device.Name) + "|" + regexp.QuoteMeta(kept)
	signer := registerPublicKey(t, compose, ".*", requests.PublicKeyFilter{Hostname: pattern})
	member := newMember(t, compose, "renamer", authorizer.RoleOperator)

	requireDeviceName := func(t *testing.T, name string) {
		t.Helper()

		current, resp, err := compose.GetDevice(t.Context(), device.UID)
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
		assert.Equal(t, name, current.Name)
	}

	rename := func(t *testing.T, name string) int {
		t.Helper()

		resp, err := asBearer(t, compose, member.Token).SetBody(map[string]string{"name": name}).Put("/api/devices/" + device.UID)
		require.NoError(t, err)

		return resp.StatusCode()
	}

	t.Run("an operator does not rename a device out of a key's name pattern", func(t *testing.T) {
		t.Cleanup(func() {
			_, _ = compose.R(context.WithoutCancel(t.Context())).SetBody(map[string]string{"name": device.Name}).Put("/api/devices/" + device.UID)
		})

		assert.Equal(t, http.StatusForbidden, rename(t, "renamed-away"))
		requireDeviceName(t, device.Name)
		requireKeyLogsIn(t, compose, deviceSSHID(device), signer)
	})

	t.Run("an operator renames a device within a key's name pattern", func(t *testing.T) {
		t.Cleanup(func() {
			_, _ = compose.R(context.WithoutCancel(t.Context())).SetBody(map[string]string{"name": device.Name}).Put("/api/devices/" + device.UID)
		})

		require.Equal(t, http.StatusOK, rename(t, kept))
		requireDeviceName(t, kept)
	})
}