Ctrl K
ComunidadGuías…

coderd/authlink/authlink_test.go

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

Archivo original ↗Preguntar a KiBot
package authlink_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/coderd/authlink"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbgen"
	"github.com/coder/coder/v2/coderd/database/dbtestutil"
	"github.com/coder/coder/v2/testutil"
)

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

	t.Run("MixedIssuers", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)
		const expectedIssuer = "https://accounts.google.com"

		// 3 users linked to the expected issuer.
		for i := 0; i < 3; i++ {
			user := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
			dbgen.UserLink(t, db, database.UserLink{
				UserID:    user.ID,
				LoginType: database.LoginTypeOIDC,
				LinkedID:  expectedIssuer + "||sub-" + user.ID.String(),
			})
		}

		// 2 users linked to an old issuer.
		for i := 0; i < 2; i++ {
			user := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
			dbgen.UserLink(t, db, database.UserLink{
				UserID:    user.ID,
				LoginType: database.LoginTypeOIDC,
				LinkedID:  "https://old-issuer.example.com||sub-" + user.ID.String(),
			})
		}

		// 1 user linked to another old issuer.
		user := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    user.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "https://staging.example.com||sub-" + user.ID.String(),
		})

		// 1 unlinked user (empty linked_id).
		unlinkedUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    unlinkedUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "",
		})

		analysis, err := authlink.AnalyzeOIDCLinks(ctx, db, expectedIssuer)
		require.NoError(t, err)

		require.Equal(t, 7, analysis.Total)
		require.Equal(t, 3, analysis.CorrectIssuer)
		require.Equal(t, 1, analysis.Unlinked)
		require.Equal(t, 3, analysis.MismatchedTotal())
		require.Equal(t, 2, analysis.MismatchedCounts["https://old-issuer.example.com"])
		require.Equal(t, 1, analysis.MismatchedCounts["https://staging.example.com"])
	})

	t.Run("NoOIDCUsers", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)

		analysis, err := authlink.AnalyzeOIDCLinks(ctx, db, "https://issuer.example.com")
		require.NoError(t, err)
		require.Equal(t, 0, analysis.Total)
		require.Equal(t, 0, analysis.CorrectIssuer)
		require.Equal(t, 0, analysis.Unlinked)
		require.Equal(t, 0, analysis.MismatchedTotal())
	})

	t.Run("AllCorrect", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)
		const expectedIssuer = "https://accounts.google.com"

		for i := 0; i < 3; i++ {
			user := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
			dbgen.UserLink(t, db, database.UserLink{
				UserID:    user.ID,
				LoginType: database.LoginTypeOIDC,
				LinkedID:  expectedIssuer + "||sub-" + user.ID.String(),
			})
		}

		analysis, err := authlink.AnalyzeOIDCLinks(ctx, db, expectedIssuer)
		require.NoError(t, err)
		require.Equal(t, 3, analysis.Total)
		require.Equal(t, 3, analysis.CorrectIssuer)
		require.Equal(t, 0, analysis.Unlinked)
		require.Equal(t, 0, analysis.MismatchedTotal())
	})

	t.Run("DeletedUsersExcluded", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)
		const expectedIssuer = "https://accounts.google.com"

		// Active user with mismatched issuer.
		activeUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    activeUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "https://old-issuer.example.com||sub-active",
		})

		// Create user and link first, then soft-delete the user.
		// The DB trigger prevents inserting links for already-deleted users.
		deletedUser := dbgen.User(t, db, database.User{
			LoginType: database.LoginTypeOIDC,
		})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    deletedUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "https://old-issuer.example.com||sub-deleted",
		})
		require.NoError(t, db.UpdateUserDeletedByID(ctx, deletedUser.ID))

		analysis, err := authlink.AnalyzeOIDCLinks(ctx, db, expectedIssuer)
		require.NoError(t, err)
		require.Equal(t, 1, analysis.Total)
		require.Equal(t, 1, analysis.MismatchedTotal())
	})

	t.Run("NonOIDCLinksExcluded", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)
		const expectedIssuer = "https://accounts.google.com"

		// GitHub user link should not be counted.
		ghUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeGithub})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    ghUser.ID,
			LoginType: database.LoginTypeGithub,
			LinkedID:  "github||12345",
		})

		analysis, err := authlink.AnalyzeOIDCLinks(ctx, db, expectedIssuer)
		require.NoError(t, err)
		require.Equal(t, 0, analysis.Total)
	})
}

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

	t.Run("ResetsOnlyMismatched", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)
		const expectedIssuer = "https://accounts.google.com"

		// Correctly linked user.
		correctUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		correctLink := dbgen.UserLink(t, db, database.UserLink{
			UserID:    correctUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  expectedIssuer + "||sub-correct",
		})

		// Mismatched user.
		mismatchedUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    mismatchedUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "https://old-issuer.example.com||sub-mismatched",
		})

		// Unlinked user (empty linked_id).
		unlinkedUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    unlinkedUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "",
		})

		count, err := authlink.ResetMismatchedOIDCLinks(ctx, db, expectedIssuer)
		require.NoError(t, err)
		require.EqualValues(t, 1, count)

		// Verify the correct link is unchanged.
		link, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
			UserID:    correctUser.ID,
			LoginType: database.LoginTypeOIDC,
		})
		require.NoError(t, err)
		require.Equal(t, correctLink.LinkedID, link.LinkedID)

		// Verify the mismatched link was reset.
		link, err = db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
			UserID:    mismatchedUser.ID,
			LoginType: database.LoginTypeOIDC,
		})
		require.NoError(t, err)
		require.Equal(t, "", link.LinkedID)

		// Verify the unlinked user is still unlinked.
		link, err = db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
			UserID:    unlinkedUser.ID,
			LoginType: database.LoginTypeOIDC,
		})
		require.NoError(t, err)
		require.Equal(t, "", link.LinkedID)
	})

	t.Run("NothingToReset", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)
		const expectedIssuer = "https://accounts.google.com"

		user := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    user.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  expectedIssuer + "||sub-correct",
		})

		count, err := authlink.ResetMismatchedOIDCLinks(ctx, db, expectedIssuer)
		require.NoError(t, err)
		require.EqualValues(t, 0, count)
	})
}

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

	t.Run("ResetsAll", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)

		// Correctly linked user.
		correctUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    correctUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "https://accounts.google.com||sub-correct",
		})

		// Mismatched user.
		mismatchedUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    mismatchedUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "https://old-issuer.example.com||sub-mismatched",
		})

		// Unlinked user (empty linked_id).
		unlinkedUser := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    unlinkedUser.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "",
		})

		count, err := authlink.ResetMismatchedOIDCLinks(ctx, db, authlink.UnmatchableIssuer)
		require.NoError(t, err)
		require.EqualValues(t, 2, count, "should reset correct + mismatched, not unlinked")

		// Verify the correct link was reset.
		link, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
			UserID:    correctUser.ID,
			LoginType: database.LoginTypeOIDC,
		})
		require.NoError(t, err)
		require.Equal(t, "", link.LinkedID)

		// Verify the mismatched link was reset.
		link, err = db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
			UserID:    mismatchedUser.ID,
			LoginType: database.LoginTypeOIDC,
		})
		require.NoError(t, err)
		require.Equal(t, "", link.LinkedID)

		// Verify the unlinked user is still unlinked.
		link, err = db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
			UserID:    unlinkedUser.ID,
			LoginType: database.LoginTypeOIDC,
		})
		require.NoError(t, err)
		require.Equal(t, "", link.LinkedID)
	})

	t.Run("NothingToReset", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		db, _ := dbtestutil.NewDB(t)

		// Only an unlinked user.
		user := dbgen.User(t, db, database.User{LoginType: database.LoginTypeOIDC})
		dbgen.UserLink(t, db, database.UserLink{
			UserID:    user.ID,
			LoginType: database.LoginTypeOIDC,
			LinkedID:  "",
		})

		count, err := authlink.ResetMismatchedOIDCLinks(ctx, db, authlink.UnmatchableIssuer)
		require.NoError(t, err)
		require.EqualValues(t, 0, count)
	})
}

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

	t.Run("Success", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		const expectedIssuer = "https://accounts.google.com"
		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.URL.Path != "/.well-known/openid-configuration" {
				http.NotFound(w, r)
				return
			}
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]string{
				"issuer": expectedIssuer,
			})
		}))
		defer srv.Close()

		issuer, err := authlink.ResolveIssuer(ctx, srv.Client(), srv.URL)
		require.NoError(t, err)
		require.Equal(t, expectedIssuer, issuer)
	})

	t.Run("EmptyIssuer", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]string{
				"issuer": "",
			})
		}))
		defer srv.Close()

		_, err := authlink.ResolveIssuer(ctx, srv.Client(), srv.URL)
		require.Error(t, err)
		require.Contains(t, err.Error(), "empty issuer")
	})

	t.Run("HTTPError", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)

		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusInternalServerError)
		}))
		defer srv.Close()

		_, err := authlink.ResolveIssuer(ctx, srv.Client(), srv.URL)
		require.Error(t, err)
		require.Contains(t, err.Error(), "HTTP 500")
	})
}