Ctrl K
ComunidadGuías…

coderd/authlink/authlink.go

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

Archivo original ↗Preguntar a KiBot
package authlink

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"

	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/database"
)

// OIDCLinkAnalysis contains the results of analyzing OIDC user links
// grouped by their issuer prefix.
type OIDCLinkAnalysis struct {
	Total            int            // Total OIDC user links
	Unlinked         int            // linked_id == ""
	CorrectIssuer    int            // linked_id starts with expectedIssuer||
	MismatchedCounts map[string]int // issuer -> count for non-matching issuers
}

// MismatchedTotal returns the total number of links with a non-matching issuer.
func (a OIDCLinkAnalysis) MismatchedTotal() int {
	total := 0
	for _, count := range a.MismatchedCounts {
		total += count
	}
	return total
}

// AnalyzeOIDCLinks queries OIDC user links grouped by issuer prefix and
// categorizes them relative to expectedIssuer.
func AnalyzeOIDCLinks(ctx context.Context, db database.Store, expectedIssuer string) (OIDCLinkAnalysis, error) {
	rows, err := db.CountOIDCLinkedIDsByIssuer(ctx)
	if err != nil {
		return OIDCLinkAnalysis{}, xerrors.Errorf("count OIDC linked IDs by issuer: %w", err)
	}

	analysis := OIDCLinkAnalysis{
		MismatchedCounts: make(map[string]int),
	}
	for _, row := range rows {
		count := int(row.Count)
		analysis.Total += count
		switch row.IssuerPrefix {
		case "":
			analysis.Unlinked += count
		case expectedIssuer:
			analysis.CorrectIssuer += count
		default:
			analysis.MismatchedCounts[row.IssuerPrefix] += count
		}
	}
	return analysis, nil
}

// ResetMismatchedOIDCLinks resets linked_id to empty for all OIDC links whose
// issuer prefix does not match expectedIssuer. Returns the number of rows
// affected.
func ResetMismatchedOIDCLinks(ctx context.Context, db database.Store, expectedIssuer string) (int64, error) {
	prefix := expectedIssuer + "||"
	count, err := db.UnlinkOIDCUsersByIssuerMismatch(ctx, prefix)
	if err != nil {
		return 0, xerrors.Errorf("unlink OIDC users by issuer mismatch: %w", err)
	}
	return count, nil
}

// UnmatchableIssuer is a synthetic issuer value that no real OIDC linked_id
// will ever start with. Passing it to AnalyzeOIDCLinks or
// ResetMismatchedOIDCLinks causes every link to be treated as "mismatched",
// which effectively resets all of them.
const UnmatchableIssuer = "00000000-0000-0000-0000-000000000000"

// ResolveIssuer uses OIDC discovery to fetch the canonical issuer string
// from the provider's .well-known/openid-configuration endpoint.
// This does not require OIDC client credentials.
//
// This works the same as `oidc.NewProvider`. The `oidc` package does not
// expose a method to extract the Issuer. So we have to manually make the
// http request.
func ResolveIssuer(ctx context.Context, cli *http.Client, issuerURL string) (string, error) {
	wellKnownURL, err := url.JoinPath(issuerURL, "/.well-known/openid-configuration")
	if err != nil {
		return "", xerrors.Errorf("resolve issuer URL: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnownURL, nil)
	if err != nil {
		return "", xerrors.Errorf("create discovery request: %w", err)
	}

	resp, err := cli.Do(req)
	if err != nil {
		return "", xerrors.Errorf("fetch OIDC discovery document: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", xerrors.Errorf("OIDC discovery returned HTTP %d", resp.StatusCode)
	}

	var discovery struct {
		Issuer string `json:"issuer"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&discovery); err != nil {
		return "", xerrors.Errorf("decode OIDC discovery document: %w", err)
	}
	if discovery.Issuer == "" {
		return "", xerrors.New("OIDC discovery document has empty issuer field")
	}
	return discovery.Issuer, nil
}

// PrintAnalysis writes a human-readable summary of the OIDC link analysis.
// Used for the cli command and debugging.
func PrintAnalysis(w io.Writer, analysis OIDCLinkAnalysis, issuer string) {
	_, _ = fmt.Fprintf(w, "OIDC Link Analysis (issuer: %s)\n", issuer)
	_, _ = fmt.Fprintf(w, "  Total OIDC users:            %d\n", analysis.Total)
	_, _ = fmt.Fprintf(w, "  Correctly linked:            %d\n", analysis.CorrectIssuer)
	_, _ = fmt.Fprintf(w, "  Unlinked (empty linked_id):  %d\n", analysis.Unlinked)

	mismatchedTotal := analysis.MismatchedTotal()
	_, _ = fmt.Fprintf(w, "  Linked to other issuers:     %d\n", mismatchedTotal)

	if mismatchedTotal > 0 {
		// Sort issuer keys for deterministic output.
		issuers := make([]string, 0, len(analysis.MismatchedCounts))
		for issuer := range analysis.MismatchedCounts {
			issuers = append(issuers, issuer)
		}
		sort.Strings(issuers)
		for _, iss := range issuers {
			_, _ = fmt.Fprintf(w, "    %s: %d\n", iss, analysis.MismatchedCounts[iss])
		}
	}
}