Writes passwords.dex.coreos.com CRs against the in-cluster Kubernetes API using the pod's service-account token + cluster CA (no kubectl / client-go dependency). NewPasswordClient returns ErrNotInCluster off cluster so the web layer degrades gracefully in dev. Load-bearing: Dex's kubernetes storage types Password.Hash as []byte, which k8s JSON-marshals as base64 — so the `hash` field carries the base64 of the bcrypt string, not the raw string. Storing the raw string makes Dex's base64-decode-on-login produce garbage and every login fail. 409 -> ErrPasswordExists, 401/403 -> ErrForbidden (RBAC missing) so the handler can give precise messages. Tested against an httptest TLS server. bcrypt cost-12 hashing lives in the web handler; golang.org/x/crypto was already a transitive dep (now promoted in go.sum). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
3.4 KiB
Go
105 lines
3.4 KiB
Go
package dex
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"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-dot-user-at-example-dot-com", gotBody.Metadata["name"])
|
|
require.Equal(t, "auth", gotBody.Metadata["namespace"])
|
|
|
|
// The hash is the BASE64 of the bcrypt string (Dex stores hash as []byte).
|
|
decoded, err := base64.StdEncoding.DecodeString(gotBody.Hash)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "$2a$12$abcdefghijklmnopqrstuv", string(decoded))
|
|
}
|
|
|
|
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) {
|
|
cases := map[string]string{
|
|
"Alice@Example.com": "alice-at-example-dot-com",
|
|
"a.b+c@gmail.com": "a-dot-b-c-at-gmail-dot-com",
|
|
"UPPER@DOMAIN.IO": "upper-at-domain-dot-io",
|
|
}
|
|
for in, want := range cases {
|
|
require.Equal(t, want, passwordName(in), in)
|
|
}
|
|
}
|