Ctrl K
ComunidadGuías…

tests/identity_approval_test.go

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

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"testing"
	"time"

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

const strayConfirmationCode = "23456789"

// TestSSHApprovalIdentity covers the approval that turns an unknown key into an identity, past the
// confirmation that lets the login in: a rejection or a lapsed window refuses it, a confirmation
// can bound the key's life, a key approved twice binds to one person only, only a member allowed
// to approve can decide and only a member can see the request, and the code the terminal shows is
// the one the console shows. Holding an unknown key and confirming it are covered by
// [TestIdentityAccessPolicy].
func TestSSHApprovalIdentity(t *testing.T) {
	t.Parallel()

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

	sshid := deviceSSHID(device)
	owner := compose.AuthUser(t, ShellHubUsername, ShellHubPassword)

	t.Run("a rejected approval refuses the login and enrolls nothing", func(t *testing.T) {
		signer, _ := newSigner(t)
		parked := startLogin(t, compose, sshid, signer)
		prompt := parked.awaitApproval(t)

		resp, err := rejectApprovalAs(t.Context(), compose, "", prompt.code)
		require.NoError(t, err)
		require.Equal(t, 200, resp.StatusCode(), resp.String())

		parked.answer(strayConfirmationCode)

		require.Error(t, parked.result(t))
		assert.Equal(t, rejectedReason, parked.denial())
		assert.Equal(t, models.SSHApprovalRejected, approvalState(t, compose, prompt.code))
		assert.Empty(t, identitiesHolding(t, compose, ssh.FingerprintSHA256(signer.PublicKey())))
	})

	t.Run("an approval past its window can no longer be confirmed and refuses the login", func(t *testing.T) {
		signer, _ := newSigner(t)
		parked := startLogin(t, compose, sshid, signer)
		prompt := parked.awaitApproval(t)

		pending, resp, err := getApprovalAs(t.Context(), compose, "", prompt.code)
		require.NoError(t, err)
		require.Equal(t, 200, resp.StatusCode(), resp.String())
		require.Equal(t, models.SSHApprovalPending, pending.State)
		require.Positive(t, pending.ExpiresIn)
		require.LessOrEqual(t, pending.ExpiresIn, 90)

		compose.ExpireSSHApproval(t, prompt.code)

		_, resp, err = confirmApprovalAs(t.Context(), compose, "", prompt.code, nil)
		require.NoError(t, err)
		assert.Equal(t, 404, resp.StatusCode(), resp.String())

		parked.answer(strayConfirmationCode)

		require.Error(t, parked.result(t))
		assert.Equal(t, expiredReason, parked.denial())
		assert.Empty(t, identitiesHolding(t, compose, ssh.FingerprintSHA256(signer.PublicKey())))
	})

	t.Run("a confirmation with an expiry enrolls a key that stops working when it expires", func(t *testing.T) {
		signer, _ := newSigner(t)
		fingerprint := ssh.FingerprintSHA256(signer.PublicKey())

		parked := startLogin(t, compose, sshid, signer)
		prompt := parked.awaitApproval(t)

		days := 1
		confirmation, resp, err := confirmApprovalAs(t.Context(), compose, "", prompt.code, &days)
		require.NoError(t, err)
		require.Equal(t, 200, resp.StatusCode(), resp.String())

		parked.answer(confirmation.ConfirmationCode)
		require.NoError(t, parked.result(t))

		identity := identityByFingerprint(t, compose, fingerprint)
		assert.Equal(t, models.SSHIdentitySourceApproval, identity.Source)
		require.NotNil(t, identity.ExpiresAt)
		assert.WithinDuration(t, time.Now().Add(24*time.Hour), *identity.ExpiresAt, time.Minute) //nolint:forbidigo // the expiry the server set from its own wall clock

		compose.ExpireSSHIdentity(t, fingerprint)

		mark := compose.ServerLogMark(t)

		refused := startLogin(t, compose, sshid, signer)
		require.Error(t, refused.result(t))
		assert.Zero(t, refused.approvals(), "an expired key must not be sent to enrollment again")

		compose.AwaitServerLogLine(t, mark, deadIdentityLog, `error="ssh access denied by policy"`)
	})

	t.Run("approving the same key twice for the same person enrolls it once", func(t *testing.T) {
		signer, _ := newSigner(t)

		first := startLogin(t, compose, sshid, signer)
		second := startLogin(t, compose, sshid, signer)

		firstPrompt := first.awaitApproval(t)
		secondPrompt := second.awaitApproval(t)
		require.NotEqual(t, firstPrompt.code, secondPrompt.code)

		first.answer(confirmApproval(t, compose, firstPrompt.code))
		second.answer(confirmApproval(t, compose, secondPrompt.code))

		require.NoError(t, first.result(t))
		require.NoError(t, second.result(t))

		fingerprint := ssh.FingerprintSHA256(signer.PublicKey())
		assert.Len(t, identitiesHolding(t, compose, fingerprint), 1)
		assert.Equal(t, owner.ID, identityByFingerprint(t, compose, fingerprint).PrincipalID)
	})

	t.Run("approving a key already bound to someone else fails and leaves it bound", func(t *testing.T) {
		member := newMember(t, compose, "approver", authorizer.RoleOperator)
		signer, _ := newSigner(t)

		first := startLogin(t, compose, sshid, signer)
		second := startLogin(t, compose, sshid, signer)

		firstPrompt := first.awaitApproval(t)
		secondPrompt := second.awaitApproval(t)

		first.answer(confirmApproval(t, compose, firstPrompt.code))
		require.NoError(t, first.result(t))

		_, resp, err := confirmApprovalAs(t.Context(), compose, member.Token, secondPrompt.code, nil)
		require.NoError(t, err)
		assert.Equal(t, 409, resp.StatusCode(), resp.String())

		assert.Equal(t, models.SSHApprovalPending, approvalState(t, compose, secondPrompt.code),
			"a failed confirmation must not consume the approval")

		fingerprint := ssh.FingerprintSHA256(signer.PublicKey())
		assert.Len(t, identitiesHolding(t, compose, fingerprint), 1)
		assert.Equal(t, owner.ID, identityByFingerprint(t, compose, fingerprint).PrincipalID)
	})

	t.Run("a member whose role cannot approve can neither confirm nor reject", func(t *testing.T) {
		observer := newMember(t, compose, "watcher", authorizer.RoleObserver)
		signer, _ := newSigner(t)

		parked := startLogin(t, compose, sshid, signer)
		prompt := parked.awaitApproval(t)

		_, resp, err := confirmApprovalAs(t.Context(), compose, observer.Token, prompt.code, nil)
		require.NoError(t, err)
		assert.Equal(t, 403, resp.StatusCode(), resp.String())

		resp, err = rejectApprovalAs(t.Context(), compose, observer.Token, prompt.code)
		require.NoError(t, err)
		assert.Equal(t, 403, resp.StatusCode(), resp.String())

		assert.Equal(t, models.SSHApprovalPending, approvalState(t, compose, prompt.code))
		assert.Empty(t, identitiesHolding(t, compose, ssh.FingerprintSHA256(signer.PublicKey())))

		parked.answer(confirmApproval(t, compose, prompt.code))
		require.NoError(t, parked.result(t))
	})

	t.Run("a person outside the namespace can neither see nor decide its approval", func(t *testing.T) {
		compose.NewUser(t, "outsider", "outsider@ossystems.com.br", ShellHubPassword)
		compose.NewNamespace(t, "outsider", "elsewhere", uuid.Generate())
		outsider := compose.AuthUser(t, "outsider", ShellHubPassword)

		signer, _ := newSigner(t)
		parked := startLogin(t, compose, sshid, signer)
		prompt := parked.awaitApproval(t)

		_, resp, err := getApprovalAs(t.Context(), compose, outsider.Token, prompt.code)
		require.NoError(t, err)
		assert.Equal(t, 404, resp.StatusCode(), resp.String())
		assert.NotContains(t, resp.String(), ssh.FingerprintSHA256(signer.PublicKey()))

		_, resp, err = confirmApprovalAs(t.Context(), compose, outsider.Token, prompt.code, nil)
		require.NoError(t, err)
		assert.Equal(t, 404, resp.StatusCode(), resp.String())

		resp, err = rejectApprovalAs(t.Context(), compose, outsider.Token, prompt.code)
		require.NoError(t, err)
		assert.Equal(t, 404, resp.StatusCode(), resp.String())

		assert.Equal(t, models.SSHApprovalPending, approvalState(t, compose, prompt.code))
	})

	t.Run("the console shows the security code and key the terminal shows", func(t *testing.T) {
		signer, _ := newSigner(t)
		fingerprint := ssh.FingerprintSHA256(signer.PublicKey())

		parked := startLogin(t, compose, sshid, signer)
		prompt := parked.awaitApproval(t)

		assert.Contains(t, prompt.instruction, "Security code:  "+prompt.code[:4]+" "+prompt.code[4:])
		assert.Contains(t, prompt.instruction, "Key:            "+fingerprint)

		approval, resp, err := getApprovalAs(t.Context(), compose, "", prompt.code)
		require.NoError(t, err)
		require.Equal(t, 200, resp.StatusCode(), resp.String())

		assert.Equal(t, prompt.code, approval.Code)
		assert.Equal(t, fingerprint, approval.Fingerprint)
		assert.Equal(t, models.SSHApprovalIdentity, approval.Kind)
		assert.Equal(t, sshid, approval.SSHID)
		assert.Equal(t, ShellHubNamespaceName, approval.Namespace)
		assert.Equal(t, device.Name, approval.DeviceName)
		assert.Equal(t, ShellHubAgentUsername, approval.Username)
	})
}