Tapir used human-readable substitutions ('@' -> '-at-', '.' -> '-dot-') when
deriving the Password CR name from an email. Dex's internal passwordID() maps
every non-[a-z0-9-] character to plain '-'. This caused a name mismatch:
Tapir wrote the CR as 'mathias-at-d-ma-dot-be', Dex looked it up as
'mathias-d-ma-be', got not-found, and returned 'Invalid credentials' on every
invite login — while static configmap passwords (a different code path) worked
fine. Diagnosed by adding the email to staticPasswords and confirming login
succeeded, proving the kubernetes CR lookup was the failure point.
106 lines
3.6 KiB
Go
106 lines
3.6 KiB
Go
package dex
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// newTestClient points a PasswordClient at an httptest server, using that
|
|
// server's TLS client so the in-cluster TLS path is exercised without a real CA.
|
|
func newTestClient(srv *httptest.Server) *PasswordClient {
|
|
return newClient(srv.URL, "test-token", srv.Client())
|
|
}
|
|
|
|
func TestCreatePasswordSuccess(t *testing.T) {
|
|
var gotAuth, gotPath, gotMethod string
|
|
var gotBody password
|
|
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAuth, gotPath, gotMethod = r.Header.Get("Authorization"), r.URL.Path, r.Method
|
|
b, _ := io.ReadAll(r.Body)
|
|
_ = json.Unmarshal(b, &gotBody)
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = w.Write([]byte(`{"kind":"Password"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
err := newTestClient(srv).CreatePassword(context.Background(),
|
|
"New.User@Example.com", "$2a$12$abcdefghijklmnopqrstuv", "user-uuid-1")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, http.MethodPost, gotMethod)
|
|
require.Equal(t, passwordsPath, gotPath)
|
|
require.Equal(t, "Bearer test-token", gotAuth)
|
|
|
|
// Email/username carry the raw address; the CR name is sanitised + lowercased.
|
|
require.Equal(t, "New.User@Example.com", gotBody.Email)
|
|
require.Equal(t, "New.User@Example.com", gotBody.Username)
|
|
require.Equal(t, "user-uuid-1", gotBody.UserID)
|
|
require.Equal(t, "new-user-example-com", gotBody.Metadata["name"])
|
|
require.Equal(t, "auth", gotBody.Metadata["namespace"])
|
|
|
|
// Hash is stored as the raw bcrypt string — Dex compares it directly.
|
|
require.Equal(t, "$2a$12$abcdefghijklmnopqrstuv", gotBody.Hash)
|
|
}
|
|
|
|
func TestCreatePasswordConflict(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusConflict)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
err := newTestClient(srv).CreatePassword(context.Background(), "dup@example.com", "$2a$12$x", "u")
|
|
require.ErrorIs(t, err, ErrPasswordExists)
|
|
}
|
|
|
|
func TestCreatePasswordForbidden(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
err := newTestClient(srv).CreatePassword(context.Background(), "x@example.com", "$2a$12$x", "u")
|
|
require.ErrorIs(t, err, ErrForbidden)
|
|
}
|
|
|
|
func TestCreatePasswordUnexpectedStatus(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("boom"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
err := newTestClient(srv).CreatePassword(context.Background(), "x@example.com", "$2a$12$x", "u")
|
|
require.Error(t, err)
|
|
require.NotErrorIs(t, err, ErrPasswordExists)
|
|
require.NotErrorIs(t, err, ErrForbidden)
|
|
require.Contains(t, err.Error(), "500")
|
|
}
|
|
|
|
func TestNewPasswordClientNotInCluster(t *testing.T) {
|
|
// In the test environment the SA token mount does not exist.
|
|
_, err := NewPasswordClient()
|
|
require.ErrorIs(t, err, ErrNotInCluster)
|
|
}
|
|
|
|
func TestPasswordName(t *testing.T) {
|
|
// Must match Dex's internal passwordID() — maps every non-[a-z0-9-] to '-'.
|
|
// Using a different scheme (e.g. '-at-', '-dot-') causes a name mismatch:
|
|
// Tapir writes the CR under one name, Dex looks it up under another.
|
|
cases := map[string]string{
|
|
"Alice@Example.com": "alice-example-com",
|
|
"a.b+c@gmail.com": "a-b-c-gmail-com",
|
|
"UPPER@DOMAIN.IO": "upper-domain-io",
|
|
"mathias@d-ma.be": "mathias-d-ma-be",
|
|
}
|
|
for in, want := range cases {
|
|
require.Equal(t, want, passwordName(in), in)
|
|
}
|
|
}
|