Ctrl K
ComunidadGuías…

tests/api_key_ssh_test.go

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

Archivo original ↗Preguntar a KiBot
package main

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

	"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"
)

// TestAPIKeySSHAccess covers what decides an API key's SSH login in a new namespace: no policy
// naming the key lets it in, a deny naming it beats the allow that would, its identity works until
// its expiry, and deleting the key takes its identities with it. That an every-member policy does
// not grant a key is covered by [TestAccessPolicyEvaluation], single-use identities by
// [TestSSHIdentityConnection], and that a key is never asked to re-authenticate by
// [TestAccessPolicyReauth].
func TestAPIKeySSHAccess(t *testing.T) {
	t.Parallel()

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

	sshid := deviceSSHID(device)

	t.Run("a key no policy names is refused", func(t *testing.T) {
		key, signer := newAPIKeyIdentity(t, compose, "ungranted", false)

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

	t.Run("a deny policy naming the key beats the allow that names it", func(t *testing.T) {
		key, signer := newAPIKeyIdentity(t, compose, "denied", false)

		grant(t, compose, &requests.AccessPolicyCreate{
			Name:    "allowed",
			Subject: apiKeySubject(key.ID),
			Filter:  requests.AccessPolicyFilter{All: true},
			Logins:  []string{"*"},
		})

		requireStraightThrough(t, compose, sshid, signer)

		grant(t, compose, &requests.AccessPolicyCreate{
			Name:    "denied",
			Subject: apiKeySubject(key.ID),
			Filter:  requests.AccessPolicyFilter{All: true},
			Logins:  []string{"*"},
			Action:  string(models.PolicyActionDeny),
		})

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

	t.Run("a key's identity is accepted until its expiry and refused after", func(t *testing.T) {
		key, signer := enrollAPIKeyIdentity(t, compose, "expiring", new(1), false)

		grant(t, compose, &requests.AccessPolicyCreate{
			Name:    "expiring",
			Subject: apiKeySubject(key.ID),
			Filter:  requests.AccessPolicyFilter{All: true},
			Logins:  []string{"*"},
		})

		live := apiKeyIdentity(t, compose, key.Name)
		require.NotNil(t, live.ExpiresAt)
		require.True(t, live.Active(time.Now()), //nolint:forbidigo // the expiry the server compares against its own wall clock
			"an identity a day from its expiry should be live")

		requireStraightThrough(t, compose, sshid, signer)

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

	t.Run("deleting a key revokes its identities", func(t *testing.T) {
		key, signer := newAPIKeyIdentity(t, compose, "revoked", false)

		policy := compose.CreateAccessPolicy(t, &requests.AccessPolicyCreate{
			Name:    "revoked",
			Subject: apiKeySubject(key.ID),
			Filter:  requests.AccessPolicyFilter{All: true},
			Logins:  []string{"*"},
		})

		requireStraightThrough(t, compose, sshid, signer)

		resp, err := compose.R(t.Context()).Delete("/api/namespaces/api-key/" + key.Name)
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())

		resp, err = compose.R(t.Context()).Get("/api/access-policies/" + policy.ID)
		require.NoError(t, err)
		assert.Equal(t, http.StatusNotFound, resp.StatusCode(), "the policy naming the key should go with it: %s", resp.String())

		unknown := startLogin(t, compose, sshid, signer)
		prompt := unknown.awaitApproval(t)
		assert.Equal(t, models.SSHApprovalIdentity, prompt.kind,
			"the gateway should no longer know the key, and ask to enroll it as a new one")
	})
}