From c6ac270266ae19f7a1b399adf80612f6103cfe43 Mon Sep 17 00:00:00 2001 From: Aravinda-HWK Date: Tue, 17 Mar 2026 15:39:27 +0530 Subject: [PATCH 1/2] feat: add support for group address validation in user-exists map and implement group validation logic --- services/socketmap/README.md | 6 +- services/socketmap/internal/handler/user.go | 21 +++- services/socketmap/internal/thunder/group.go | 120 +++++++++++++++++++ services/socketmap/internal/thunder/types.go | 16 +++ 4 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 services/socketmap/internal/thunder/group.go 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..aa41018 --- /dev/null +++ b/services/socketmap/internal/thunder/group.go @@ -0,0 +1,120 @@ +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) + + parts := strings.Split(email, "@") + if len(parts) != 2 { + log.Printf(" │ ✗ Invalid email format") + log.Printf(" └──────────────────────────────") + return false, nil + } + + localPart := parts[0] + domain := parts[1] + + if !strings.HasSuffix(localPart, "-group") { + log.Printf(" │ ✗ Not a group address") + log.Printf(" └──────────────────────────────") + return false, nil + } + + groupName := strings.TrimSuffix(localPart, "-group") + if groupName == "" { + log.Printf(" │ ✗ Empty group name") + log.Printf(" └──────────────────────────────") + 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) + log.Printf(" └──────────────────────────────") + return false, err + } + + ouID, err := GetOrgUnitIDForDomain(domain, host, port, tokenRefreshSeconds) + if err != nil { + log.Printf(" │ ⚠ Failed to get OU ID: %v", err) + log.Printf(" └──────────────────────────────") + 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) + log.Printf(" └──────────────────────────────") + 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) + log.Printf(" └──────────────────────────────") + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Printf(" │ ⚠ Unexpected status: %d", resp.StatusCode) + log.Printf(" └──────────────────────────────") + 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) + log.Printf(" └──────────────────────────────") + return false, err + } + + log.Printf(" │ Total results: %d", groupsResp.TotalResults) + + if groupsResp.TotalResults == 0 { + log.Printf(" │ ✗ Group not found in Thunder") + log.Printf(" └──────────────────────────────") + return false, nil + } + + for _, group := range groupsResp.Groups { + if group.OrganizationUnitID == ouID && group.Name == groupName { + log.Printf(" │ ✓ Group found and OU matches") + log.Printf(" └──────────────────────────────") + return true, nil + } + } + + log.Printf(" │ ✗ Group found but OU/name mismatch") + log.Printf(" └──────────────────────────────") + return false, nil +} 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"` +} From 64b8151eecaa08e1feb58916b5cecc8573a5d28e Mon Sep 17 00:00:00 2001 From: Aravinda-HWK Date: Wed, 18 Mar 2026 07:43:03 +0530 Subject: [PATCH 2/2] refactor: streamline logging in ValidateGroupAddress function --- services/socketmap/internal/thunder/group.go | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/services/socketmap/internal/thunder/group.go b/services/socketmap/internal/thunder/group.go index aa41018..88da16c 100644 --- a/services/socketmap/internal/thunder/group.go +++ b/services/socketmap/internal/thunder/group.go @@ -13,11 +13,11 @@ import ( 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") - log.Printf(" └──────────────────────────────") return false, nil } @@ -26,14 +26,12 @@ func ValidateGroupAddress(email, host, port string, tokenRefreshSeconds int) (bo if !strings.HasSuffix(localPart, "-group") { log.Printf(" │ ✗ Not a group address") - log.Printf(" └──────────────────────────────") return false, nil } groupName := strings.TrimSuffix(localPart, "-group") if groupName == "" { log.Printf(" │ ✗ Empty group name") - log.Printf(" └──────────────────────────────") return false, nil } @@ -43,14 +41,12 @@ func ValidateGroupAddress(email, host, port string, tokenRefreshSeconds int) (bo auth, err := GetAuth(host, port, tokenRefreshSeconds) if err != nil { log.Printf(" │ ⚠ Auth failed: %v", err) - log.Printf(" └──────────────────────────────") return false, err } ouID, err := GetOrgUnitIDForDomain(domain, host, port, tokenRefreshSeconds) if err != nil { log.Printf(" │ ⚠ Failed to get OU ID: %v", err) - log.Printf(" └──────────────────────────────") return false, err } @@ -64,7 +60,6 @@ func ValidateGroupAddress(email, host, port string, tokenRefreshSeconds int) (bo req, err := http.NewRequest("GET", baseURL, nil) if err != nil { log.Printf(" │ ✗ Failed to create request: %v", err) - log.Printf(" └──────────────────────────────") return false, err } @@ -80,21 +75,18 @@ func ValidateGroupAddress(email, host, port string, tokenRefreshSeconds int) (bo resp, err := client.Do(req) if err != nil { log.Printf(" │ ✗ Request failed: %v", err) - log.Printf(" └──────────────────────────────") return false, err } defer resp.Body.Close() if resp.StatusCode != 200 { log.Printf(" │ ⚠ Unexpected status: %d", resp.StatusCode) - log.Printf(" └──────────────────────────────") 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) - log.Printf(" └──────────────────────────────") return false, err } @@ -102,19 +94,16 @@ func ValidateGroupAddress(email, host, port string, tokenRefreshSeconds int) (bo if groupsResp.TotalResults == 0 { log.Printf(" │ ✗ Group not found in Thunder") - log.Printf(" └──────────────────────────────") return false, nil } for _, group := range groupsResp.Groups { if group.OrganizationUnitID == ouID && group.Name == groupName { log.Printf(" │ ✓ Group found and OU matches") - log.Printf(" └──────────────────────────────") return true, nil } } log.Printf(" │ ✗ Group found but OU/name mismatch") - log.Printf(" └──────────────────────────────") return false, nil -} +} \ No newline at end of file