Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80565fdbf2 | ||
|
|
84516e9b4f |
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,6 +25,7 @@ type stubKC struct {
|
|||||||
emailCalls atomic.Int32
|
emailCalls atomic.Int32
|
||||||
healthCalls atomic.Int32
|
healthCalls atomic.Int32
|
||||||
syncCalls atomic.Int32
|
syncCalls atomic.Int32
|
||||||
|
lastOrgBody string
|
||||||
|
|
||||||
tokenFails atomic.Bool // when true, /token returns 401 once
|
tokenFails atomic.Bool // when true, /token returns 401 once
|
||||||
}
|
}
|
||||||
@@ -53,6 +55,8 @@ func newStubKC(t *testing.T) *stubKC {
|
|||||||
mux.HandleFunc("/admin/realms/test-realm/organizations", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/admin/realms/test-realm/organizations", func(w http.ResponseWriter, r *http.Request) {
|
||||||
s.orgCalls.Add(1)
|
s.orgCalls.Add(1)
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
s.lastOrgBody = string(body)
|
||||||
w.Header().Set("Location", s.srv.URL+"/admin/realms/test-realm/organizations/org-xyz")
|
w.Header().Set("Location", s.srv.URL+"/admin/realms/test-realm/organizations/org-xyz")
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
return
|
return
|
||||||
@@ -135,6 +139,12 @@ func TestHTTPAdapter_createOrgAndInvite(t *testing.T) {
|
|||||||
t.Errorf("call counts: org=%d user=%d member=%d email=%d",
|
t.Errorf("call counts: org=%d user=%d member=%d email=%d",
|
||||||
s.orgCalls.Load(), s.userCalls.Load(), s.memberCalls.Load(), s.emailCalls.Load())
|
s.orgCalls.Load(), s.userCalls.Load(), s.memberCalls.Load(), s.emailCalls.Load())
|
||||||
}
|
}
|
||||||
|
// KC 26 rejects a domainless org; the adapter must send a synthetic
|
||||||
|
// per-slug domain so onboarding actually provisions.
|
||||||
|
if !strings.Contains(s.lastOrgBody, `"domains"`) ||
|
||||||
|
!strings.Contains(s.lastOrgBody, "acme.tenant.breakpilot.com") {
|
||||||
|
t.Errorf("org create body missing synthetic domain: %s", s.lastOrgBody)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHTTPAdapter_emailMissingAdminEmailRejected(t *testing.T) {
|
func TestHTTPAdapter_emailMissingAdminEmailRejected(t *testing.T) {
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import (
|
|||||||
|
|
||||||
// ─── organizations API ───────────────────────────────────────────────────
|
// ─── organizations API ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// orgDomainSuffix namespaces the synthetic org domain. The slug is unique in
|
||||||
|
// the registry, so "<slug>.tenant.breakpilot.com" is unique per organization
|
||||||
|
// and never a real deliverable mail domain we might clash with.
|
||||||
|
const orgDomainSuffix = ".tenant.breakpilot.com"
|
||||||
|
|
||||||
type orgCreate struct {
|
type orgCreate struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Alias string `json:"alias"`
|
Alias string `json:"alias"`
|
||||||
@@ -50,6 +55,16 @@ func (a *HTTPAdapter) CreateOrgAndInvite(ctx context.Context, in InviteInput) (*
|
|||||||
Name: in.Name,
|
Name: in.Name,
|
||||||
Alias: in.Slug,
|
Alias: in.Slug,
|
||||||
Description: fmt.Sprintf("Auto-provisioned from tenant-registry %s", in.TenantID),
|
Description: fmt.Sprintf("Auto-provisioned from tenant-registry %s", in.TenantID),
|
||||||
|
// Keycloak 26 rejects an organization with no domain ("You must
|
||||||
|
// provide at least one domain"). Membership is registry-authoritative
|
||||||
|
// (model B2), so we do NOT use Keycloak's email-domain auto-join; a
|
||||||
|
// synthetic per-tenant domain derived from the unique slug satisfies
|
||||||
|
// the constraint without depending on the customer's real mail domain
|
||||||
|
// (which may be a shared public domain and would collide across
|
||||||
|
// tenants). Unverified is fine — verification only gates auto-join.
|
||||||
|
Domains: []map[string]any{
|
||||||
|
{"name": in.Slug + orgDomainSuffix, "verified": false},
|
||||||
|
},
|
||||||
Attributes: map[string][]string{
|
Attributes: map[string][]string{
|
||||||
"tenant_id": {in.TenantID},
|
"tenant_id": {in.TenantID},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user