fix(dex): passwordName must match Dex's internal passwordID() — maps non-[a-z0-9-] to '-'
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.
This commit is contained in:
@@ -23,7 +23,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -158,22 +157,25 @@ func (c *PasswordClient) CreatePassword(ctx context.Context, email, bcryptHash,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// invalidNameChars matches anything not allowed in an RFC-1123 subdomain segment
|
// passwordName maps an email to the Kubernetes object name Dex uses internally
|
||||||
// after the explicit @/. substitutions, so any stray character becomes '-'.
|
// when looking up a Password CR by email (Dex storage/kubernetes passwordID()).
|
||||||
var invalidNameChars = regexp.MustCompile(`[^a-z0-9-]`)
|
// Dex maps every character that is not [a-z0-9-] to '-' — it does NOT use
|
||||||
|
// human-readable substitutions like '-at-' or '-dot-'. Using a different scheme
|
||||||
// passwordName maps an email to a valid, deterministic Kubernetes object name:
|
// creates a name mismatch: Tapir writes the CR under one name, Dex looks it up
|
||||||
// lowercase, '@' -> '-at-', '.' -> '-dot-', any remaining invalid char -> '-',
|
// under another, and every login returns "Invalid credentials".
|
||||||
// with leading/trailing '-' trimmed. Deterministic so a re-invite targets the
|
|
||||||
// same CR (and so Dex's 409 is meaningful).
|
|
||||||
func passwordName(email string) string {
|
func passwordName(email string) string {
|
||||||
n := strings.ToLower(strings.TrimSpace(email))
|
n := strings.ToLower(strings.TrimSpace(email))
|
||||||
n = strings.ReplaceAll(n, "@", "-at-")
|
var b strings.Builder
|
||||||
n = strings.ReplaceAll(n, ".", "-dot-")
|
for _, r := range n {
|
||||||
n = invalidNameChars.ReplaceAllString(n, "-")
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||||
n = strings.Trim(n, "-")
|
b.WriteRune(r)
|
||||||
if n == "" {
|
} else {
|
||||||
n = "user"
|
b.WriteRune('-')
|
||||||
}
|
}
|
||||||
return n
|
}
|
||||||
|
result := strings.Trim(b.String(), "-")
|
||||||
|
if result == "" {
|
||||||
|
return "user"
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func TestCreatePasswordSuccess(t *testing.T) {
|
|||||||
require.Equal(t, "New.User@Example.com", gotBody.Email)
|
require.Equal(t, "New.User@Example.com", gotBody.Email)
|
||||||
require.Equal(t, "New.User@Example.com", gotBody.Username)
|
require.Equal(t, "New.User@Example.com", gotBody.Username)
|
||||||
require.Equal(t, "user-uuid-1", gotBody.UserID)
|
require.Equal(t, "user-uuid-1", gotBody.UserID)
|
||||||
require.Equal(t, "new-dot-user-at-example-dot-com", gotBody.Metadata["name"])
|
require.Equal(t, "new-user-example-com", gotBody.Metadata["name"])
|
||||||
require.Equal(t, "auth", gotBody.Metadata["namespace"])
|
require.Equal(t, "auth", gotBody.Metadata["namespace"])
|
||||||
|
|
||||||
// Hash is stored as the raw bcrypt string — Dex compares it directly.
|
// Hash is stored as the raw bcrypt string — Dex compares it directly.
|
||||||
@@ -90,10 +90,14 @@ func TestNewPasswordClientNotInCluster(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPasswordName(t *testing.T) {
|
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{
|
cases := map[string]string{
|
||||||
"Alice@Example.com": "alice-at-example-dot-com",
|
"Alice@Example.com": "alice-example-com",
|
||||||
"a.b+c@gmail.com": "a-dot-b-c-at-gmail-dot-com",
|
"a.b+c@gmail.com": "a-b-c-gmail-com",
|
||||||
"UPPER@DOMAIN.IO": "upper-at-domain-dot-io",
|
"UPPER@DOMAIN.IO": "upper-domain-io",
|
||||||
|
"mathias@d-ma.be": "mathias-d-ma-be",
|
||||||
}
|
}
|
||||||
for in, want := range cases {
|
for in, want := range cases {
|
||||||
require.Equal(t, want, passwordName(in), in)
|
require.Equal(t, want, passwordName(in), in)
|
||||||
|
|||||||
Reference in New Issue
Block a user