Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions services/socketmap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<groupname>-group@domainname`.

**Caching:** Positive results cached for 5 minutes. Negative results NOT cached (new users/groups immediately accessible).

## Troubleshooting

Expand Down
21 changes: 18 additions & 3 deletions services/socketmap/internal/handler/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package handler
import (
"fmt"
"log"
"strings"
"time"

"socketmap/config"
Expand Down Expand Up @@ -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)
Expand Down
109 changes: 109 additions & 0 deletions services/socketmap/internal/thunder/group.go
Original file line number Diff line number Diff line change
@@ -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-name>-group@<domain>
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
}
16 changes: 16 additions & 0 deletions services/socketmap/internal/thunder/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Loading