diff --git a/services/socketmap/README.md b/services/socketmap/README.md index 4aa7971..3b396a0 100644 --- a/services/socketmap/README.md +++ b/services/socketmap/README.md @@ -39,11 +39,13 @@ virtual_alias_maps = socketmap:inet:socketmap-server:9100:virtual-aliases | Map | Purpose | Response | |-----|---------|----------| -| `user-exists` | Validate users via Thunder IDP | `OK email` or `NOTFOUND` | +| `user-exists` | Validate users and group addresses via Thunder IDP | `OK email` or `NOTFOUND` | | `virtual-domains` | Validate domains via Thunder OUs | `OK 1` or `NOTFOUND` | | `virtual-aliases` | Resolve aliases | `OK target` or `NOTFOUND` | -**Caching:** Positive results cached for 5 minutes. Negative results NOT cached (new users immediately accessible). +Group address format: `-group@domainname`. + +**Caching:** Positive results cached for 5 minutes. Negative results NOT cached (new users/groups immediately accessible). ## Troubleshooting diff --git a/services/socketmap/internal/handler/user.go b/services/socketmap/internal/handler/user.go index f801223..02d17f4 100644 --- a/services/socketmap/internal/handler/user.go +++ b/services/socketmap/internal/handler/user.go @@ -3,6 +3,7 @@ package handler import ( "fmt" "log" + "strings" "time" "socketmap/config" @@ -41,14 +42,28 @@ func UserExists(email string, cfg *config.Config, cacheManager *cache.Cache) boo log.Printf(" │ Querying IDP...") } - // Query Thunder IDP for user validation + // Query Thunder IDP for user validation first. exists, err := thunder.ValidateUser(email, cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) if err != nil { - log.Printf(" │ ⚠ IDP query failed: %v", err) - log.Printf(" │ User not found - Thunder unavailable") + log.Printf(" │ ⚠ User lookup failed: %v", err) exists = false } + // Treat group addresses as mailbox identities in user-exists map. + if !exists && strings.Contains(email, "@") { + groupExists, groupErr := thunder.ValidateGroupAddress(email, cfg.ThunderHost, cfg.ThunderPort, cfg.TokenRefreshSeconds) + if groupErr != nil { + log.Printf(" │ ⚠ Group lookup failed: %v", groupErr) + } else if groupExists { + log.Printf(" │ ✓ Group found; treating as existing user") + exists = true + } + } + + if !exists { + log.Printf(" │ User/group not found - Thunder unavailable or no match") + } + log.Printf(" │ IDP result: exists=%v", exists) // Only cache positive results (exists=true) diff --git a/services/socketmap/internal/thunder/group.go b/services/socketmap/internal/thunder/group.go new file mode 100644 index 0000000..88da16c --- /dev/null +++ b/services/socketmap/internal/thunder/group.go @@ -0,0 +1,109 @@ +package thunder + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strings" +) + +// ValidateGroupAddress checks if an address matches the group pattern and exists in Thunder IDP. +// Expected format: -group@ +func ValidateGroupAddress(email, host, port string, tokenRefreshSeconds int) (bool, error) { + log.Printf(" ┌─ Thunder Group Validation ────") + log.Printf(" │ Email: %s", email) + defer log.Printf(" └──────────────────────────────") + + parts := strings.Split(email, "@") + if len(parts) != 2 { + log.Printf(" │ ✗ Invalid email format") + return false, nil + } + + localPart := parts[0] + domain := parts[1] + + if !strings.HasSuffix(localPart, "-group") { + log.Printf(" │ ✗ Not a group address") + return false, nil + } + + groupName := strings.TrimSuffix(localPart, "-group") + if groupName == "" { + log.Printf(" │ ✗ Empty group name") + return false, nil + } + + log.Printf(" │ Group: %s", groupName) + log.Printf(" │ Domain: %s", domain) + + auth, err := GetAuth(host, port, tokenRefreshSeconds) + if err != nil { + log.Printf(" │ ⚠ Auth failed: %v", err) + return false, err + } + + ouID, err := GetOrgUnitIDForDomain(domain, host, port, tokenRefreshSeconds) + if err != nil { + log.Printf(" │ ⚠ Failed to get OU ID: %v", err) + return false, err + } + + log.Printf(" │ OU ID: %s", ouID) + + client := GetHTTPClient() + escapedGroupName := escapeFilterValue(groupName) + filter := fmt.Sprintf("name eq \"%s\"", escapedGroupName) + + baseURL := fmt.Sprintf("https://%s:%s/groups", host, port) + req, err := http.NewRequest("GET", baseURL, nil) + if err != nil { + log.Printf(" │ ✗ Failed to create request: %v", err) + return false, err + } + + q := req.URL.Query() + q.Add("filter", filter) + req.URL.RawQuery = q.Encode() + + req.Header.Set("Authorization", "Bearer "+auth.BearerToken) + req.Header.Set("Content-Type", "application/json") + + log.Printf(" │ Query: %s", req.URL.String()) + + resp, err := client.Do(req) + if err != nil { + log.Printf(" │ ✗ Request failed: %v", err) + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Printf(" │ ⚠ Unexpected status: %d", resp.StatusCode) + return false, fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + var groupsResp GroupsResponse + if err := json.NewDecoder(resp.Body).Decode(&groupsResp); err != nil { + log.Printf(" │ ✗ Failed to parse response: %v", err) + return false, err + } + + log.Printf(" │ Total results: %d", groupsResp.TotalResults) + + if groupsResp.TotalResults == 0 { + log.Printf(" │ ✗ Group not found in Thunder") + return false, nil + } + + for _, group := range groupsResp.Groups { + if group.OrganizationUnitID == ouID && group.Name == groupName { + log.Printf(" │ ✓ Group found and OU matches") + return true, nil + } + } + + log.Printf(" │ ✗ Group found but OU/name mismatch") + return false, nil +} \ No newline at end of file diff --git a/services/socketmap/internal/thunder/types.go b/services/socketmap/internal/thunder/types.go index e27e6a5..1d033fd 100644 --- a/services/socketmap/internal/thunder/types.go +++ b/services/socketmap/internal/thunder/types.go @@ -48,3 +48,19 @@ type User struct { Type string `json:"type"` Attributes map[string]interface{} `json:"attributes"` } + +// GroupsResponse represents the response from Thunder Groups API +type GroupsResponse struct { + TotalResults int `json:"totalResults"` + StartIndex int `json:"startIndex"` + Count int `json:"count"` + Groups []Group `json:"groups"` + Links []interface{} `json:"links"` +} + +// Group represents a Thunder group +type Group struct { + ID string `json:"id"` + Name string `json:"name"` + OrganizationUnitID string `json:"organizationUnitId"` +}