Repository navigation
fix: use an OU's handle as its mail domain - #308
maneesha-xyz wants to merge 6 commits into
Conversation
|
| parent = strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/organization-units/"), "/ous") | ||
| default: | ||
| w.WriteHeader(http.StatusNotFound) | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
Listing permissions remain untested
The fake Thunder accepts a hard-coded bearer token, so these tests cannot establish whether Raven’s service-account credentials can read both new listing endpoints. If Thunder returns 403 for any page, the first index build fails and no domains resolve. Add a contract or deployment smoke test using Raven’s credentials to catch that configuration failure before rollout.
Knowledge Base Used: Thunder directory integration
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| } else if stale && refreshing.CompareAndSwap(false, true) { | ||
| go func() { | ||
| defer refreshing.Store(false) | ||
| if err := rebuildIndex(host, port, tokenRefreshSeconds, true); err != nil { | ||
| log.Printf(" │ ⚠ Domain index refresh failed, serving the previous one: %v", err) | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| ouID, found := index[domain] |
There was a problem hiding this comment.
When an OU is added after the index is built, repeated lookups for its domain still use the old snapshot and return NOTFOUND. A lookup after the configured TTL only starts a background rebuild; it does not wait for the new domain to appear. Mail to a valid newly configured domain can therefore be rejected until that rebuild finishes.
Knowledge Base Used:
|
|
||
| if len(handles) == 0 { | ||
| return "", fmt.Errorf("no OU handles found") | ||
| domain, ok := maildomain.FromHandle(ou.Handle) |
There was a problem hiding this comment.
Duplicate domains share mailboxes
When two OUs have the same domain handle, SocketMap excludes that domain as ambiguous, but this resolver and IMAP login still accept each OU's handle. If users in those OUs share a local username, group expansion collapses their addresses into one. LMTP still delivers after SocketMap returns NOTFOUND, and both users can select the same email-keyed mailbox. Mail intended for distinct organizations can thus be mixed.
How this was verified: Both OU resolvers accept the duplicate handle, while group expansion and mailbox storage identify recipients by email rather than OU ID.
Knowledge Base Used:
| } else if stale && refreshing.CompareAndSwap(false, true) { | ||
| go func() { | ||
| defer refreshing.Store(false) | ||
| if err := rebuildIndex(host, port, tokenRefreshSeconds, true); err != nil { | ||
| log.Printf(" │ ⚠ Domain index refresh failed, serving the previous one: %v", err) | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| ouID, found := index[domain] | ||
| return ouID, found, nil |
There was a problem hiding this comment.
If an OU's handle changes after the index is built, domain validation continues to accept its former domain while a refresh is pending or failing. Address lookups also use the stored OU ID without checking its current handle, so users still in that OU can be accepted under the former domain. This now affects even domains that were never queried before the handle changed.
How this was verified: Domain validation answers from the stored index, and user validation compares against its OU ID without rereading the OU's handle.
Knowledge Base Used:
| // FromHandle returns the mail domain declared by an OU handle, lowercased and without a | ||
| // trailing dot. It reports false when the handle is not a domain. | ||
| func FromHandle(handle string) (string, bool) { | ||
| domain := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(handle)), ".") |
There was a problem hiding this comment.
Domain casing splits mailboxes
If an OU handle or a user's login domain contains uppercase letters, group expansion now lowercases the handle when forming the member's address, but LOGIN accepts the domain case-insensitively and keeps the spelling supplied by the client. Mail delivery and IMAP then open different email-keyed database files, so a user logged in with the uppercase spelling cannot see mail delivered to the lowercased address.
Knowledge Base Used:
| go func() { | ||
| if err := thunder.WarmIndex(cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds); err != nil { | ||
| log.Printf("⚠ Initial domain index build failed: %v", err) | ||
| log.Printf(" Check that raven's service account can list organization units") | ||
| log.Printf(" Service will retry on first request") | ||
| } | ||
| }() |
There was a problem hiding this comment.
If the initial OU-tree walk lasts longer than LMTP’s two-second SocketMap deadline, SocketMap accepts connections while cold lookups are still waiting for the index. LMTP then falls back to the recipient address instead of the resolved mailbox identity, so mail for a role mailbox can be stored under the wrong identity. Keep readiness from signaling usable lookups until the initial index is available.
Knowledge Base Used:
| go func() { | ||
| if err := thunder.WarmIndex(cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds); err != nil { |
There was a problem hiding this comment.
If a cold lookup starts building the index before the new startup goroutine does, WarmIndex still forces a second complete OU-tree walk after the first finishes. On a large tree, this doubles startup listing traffic to Thunder without improving the fresh index. The new test covers only the ordering where the warm build starts first.
Knowledge Base Used: Directory lookup service
📌 Description
An OU's handle is now its mail domain. Raven no longer works out the domain from where the OU sits in the Thunder tree, so a domain can have any number of labels, a child OU's domain doesn't have to be a subdomain of its parent's, and a grouping OU (such as
foundations) can sit above OUs that receive mail.🔍 Changes Made
internal/maildomain: newFromHandle, which returns an OU handle as a lowercased domain, or reports that the handle isn't a domain (foundations,default).internal/socketmap/thunder:buildOUPathand the never-expiringouCacheare removed.ValidateDomainandGetOrgUnitIDForDomainnow look the domain up in a domain → OU ID index built by walkingGET /organization-unitsandGET /organization-units/{id}/ous(paged at 100, followingnextlinks).CACHE_TTL_SECONDS. If a rebuild fails, the previous index stays in use.internal/server/authandinternal/delivery/groupresolver: the parent-walk inresolveOrganizationUnitDomainis replaced with a singleGET /organization-units/{id}andFromHandle. The callers (IMAP LOGIN domain check, OAUTHBEARER, group expansion) are unchanged.docs/THUNDER_CONSUMER_CONTRACT_GUIDE.md: endpoint [TASK] Write Test for "LOGIN" Command #3 is now the list/children endpoints, and [BUG] Copy of the send mail was not saved on the sqlite database. #9 no longer describes a parent walk.✅ Checklist (Email System)
LOGIN,CAPABILITY,LIST,SELECT,FETCH,LOGOUT).🧪 Testing Instructions
To test the server, use the instructions in the README in the
testdirectory.go test ./...passes. The new and updated tests:internal/maildomain: handle parsing, including deep domains,co.uk, mixed case, trailing dot, non-domain handles and path-traversal characters.internal/socketmap/thunder/index_test.go: a child OUsilver.x.yunderfoundationsresolves with nox.yOU present; duplicate handles resolve to nothing; pagination; index reuse within the TTL; the previous index is served when a rebuild fails; an error when no index was ever built.internal/server/authandinternal/delivery/groupresolver: fixtures use full-domain handles and fail the test if a parent OU is fetched.Against a real Thunder, with raven's own
IDP_CLIENT_ID/IDP_CLIENT_SECRET, run the socketmap service and check thatvirtual-domains <child OU domain>returnsOK 1andvirtual-domains <grouping OU handle>returnsNOTFOUND. This also confirms raven's token may call the list endpoints; so far they have only been checked with an admin token.silverunderx.ybecomessilver.x.y. Otherwise mail for that OU stops resolving and its users can't log in.CACHE_TTL_SECONDS(default 300) to take effect. Socketmap answers from the domain index, and a stale index is refreshed in the background, so Postfix rejects mail for a newly created domain until the next refresh lands. A renamed OU's old domain is accepted for the same window.mailDomainswould enforce this.mailDomainsattribute on OUs in Thunder.