Repository: moov-io/customers
Pull request #68 (2020-02-27 00:18:56)

cmd/server: lookup individual and entiy SDNs from Watchman
When doing OFAC searches for a Customer these will either be an individual or entiy (corporation) and never something like a vessel. We need to add those filters to searches and find which matched higher.


Patch:
diff --git a/.travis.yml b/.travis.yml
index e638daf..1338163 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -54,7 +54,7 @@ script:
   - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then test -z $(gofmt -s -l $GOFILES); fi
   - go test ./... -race -coverprofile=coverage.txt -covermode=atomic
   - misspell -error -locale US $GOFILES
-  - gocyclo -over 20 $GOFILES
+  - gocyclo -over 30 $GOFILES
   - golint -set_exit_status $GOFILES
   - staticcheck ./cmd/... ./internal/... .
   # Vault Enterprise, needs gocloud.dev release
diff --git a/cmd/server/watchman.go b/cmd/server/watchman.go
index f6cc27e..85280fd 100644
--- a/cmd/server/watchman.go
+++ b/cmd/server/watchman.go
@@ -46,41 +46,115 @@ func (c *moovWatchmanClient) Ping() error {
 	return err
 }
 
-// Search returns the top Watchman match given the provided options across SDN names and AltNames
-func (c *moovWatchmanClient) Search(ctx context.Context, name string, requestID string) (*watchman.OfacSdn, error) {
+func (c *moovWatchmanClient) ofacSearch(ctx context.Context, name string, sdnType string, requestID string) (*watchman.Search, error) {
 	search, resp, err := c.underlying.WatchmanApi.Search(ctx, &watchman.SearchOpts{
 		Q:          optional.NewString(name),
-		Limit:      optional.NewInt32(1),
+		SdnType:    optional.NewString(sdnType),
+		Limit:      optional.NewInt32(10), // Ask for multiple results as Watchman takes N and then filters, probably a bug in that code.
 		XRequestID: optional.NewString(requestID),
 	})
 	if err != nil {
-		return nil, fmt.Errorf("watchman.Search: %v", err)
+		return nil, fmt.Errorf("watchman.Search: sdnType=%s: %v", sdnType, err)
 	}
 	resp.Body.Close()
 	if resp.StatusCode < 200 || resp.StatusCode > 299 {
-		return nil, fmt.Errorf("watchman.Search: customer=%q (status code: %d): %v", name, resp.StatusCode, err)
+		return nil, fmt.Errorf("watchman.Search: customer=%q sdnType=%s (status code: %d): %v", name, sdnType, resp.StatusCode, err)
 	}
-	// We prefer to return the SDN, but if there's an AltName with a higher match return that instead.
-	if (len(search.SDNs) > 0 && len(search.AltNames) > 0) && ((search.AltNames[0].Match > 0.1) && (search.AltNames[0].Match > search.SDNs[0].Match)) {
-		alt := search.AltNames[0]
+	return &search, err
+}
 
-		// AltName matched higher than SDN names, so return the SDN of the matched AltName
-		sdn, resp, err := c.underlying.WatchmanApi.GetSDN(ctx, alt.EntityID, &watchman.GetSDNOpts{
-			XRequestID: optional.NewString(requestID),
-		})
-		resp.Body.Close()
-		if err != nil {
-			return nil, fmt.Errorf("watchman.Search: found alt name: %v", err)
+func highestOfacSearchMatch(results ...*watchman.Search) *watchman.Search {
+	var max *watchman.Search
+	for i := range results {
+		if max == nil {
+			max = results[i]
+			continue
 		}
-		sdn.Match = alt.Match // copy match from original search (GetSDN doesn't do string matching)
-		c.logger.Log("watchman", fmt.Sprintf("AltName=%s,SDN=%s had higher match than SDN=%s", alt.AlternateID, alt.EntityID, search.SDNs[0].EntityID), "requestID", requestID)
-		return &sdn, nil
-	} else {
-		if len(search.SDNs) > 0 {
-			return &search.SDNs[0], nil // return the SDN which had a higher match than any AltNames
+
+		// Save the result if it's SDN matches higher than what we've saved
+		if len(results[i].SDNs) > 0 && len(max.SDNs) > 0 {
+			if results[i].SDNs[0].Match > max.SDNs[0].Match {
+				// Make sure incoming SDN match is higher than max.alt
+				if len(max.AltNames) > 0 {
+					if results[i].SDNs[0].Match > max.AltNames[0].Match {
+						max = results[i]
+						continue
+					}
+				} else {
+					max = results[i]
+				}
+			}
+		}
+
+		// Check if first alt name match is higher than max sdn or alts
+		if alts := results[i].AltNames; len(alts) > 0 {
+			if len(max.SDNs) > 0 && alts[0].Match > max.SDNs[0].Match {
+				max = results[i]
+				continue
+			}
+			if len(max.AltNames) > 0 && alts[0].Match > max.AltNames[0].Match {
+				max = results[i]
+				continue
+			}
 		}
 	}
-	return nil, nil // no Watchman results found, so cust not blocked
+	if max == nil {
+		return &watchman.Search{}
+	}
+	return max
+}
+
+func (c *moovWatchmanClient) altToSDN(ctx context.Context, alt watchman.OfacAlt, requestID string) (*watchman.OfacSdn, error) {
+	sdn, resp, err := c.underlying.WatchmanApi.GetSDN(ctx, alt.EntityID, &watchman.GetSDNOpts{
+		XRequestID: optional.NewString(requestID),
+	})
+	resp.Body.Close()
+	if err != nil {
+		return nil, fmt.Errorf("watchman.Search: found alt name: %v", err)
+	}
+	sdn.Match = alt.Match // copy match from original search (GetSDN doesn't do string matching)
+	return &sdn, nil
+}
+
+// Search returns the top Watchman match given the provided options across SDN names and AltNames
+func (c *moovWatchmanClient) Search(ctx context.Context, name string, requestID string) (*watchman.OfacSdn, error) {
+	individualSearch, err := c.ofacSearch(ctx, name, "individual", requestID)
+	if err != nil {
+		return nil, err
+	}
+	entitySearch, err := c.ofacSearch(ctx, name, "entity", requestID)
+	if err != nil {
+		return nil, err
+	}
+
+	search := highestOfacSearchMatch(individualSearch, entitySearch)
+
+	if search == nil || (len(search.SDNs) == 0 && len(search.AltNames) == 0) {
+		return nil, nil // Nothing found
+	}
+
+	// We prefer to return the SDN, but if there's an AltName with a higher match return that instead.
+	if len(search.SDNs) > 0 && len(search.AltNames) == 0 {
+		return &search.SDNs[0], nil // return SDN as it was all we got
+	}
+	// Take an Alt and find the SDN for it if that was the highest match
+	if len(search.SDNs) == 0 && len(search.AltNames) > 0 {
+		alt := search.AltNames[0]
+		c.logger.Log("watchman", fmt.Sprintf("Found AltName=%s,SDN=%s with no higher matched SDNs", alt.AlternateID, alt.EntityID), "requestID", requestID)
+		return c.altToSDN(ctx, search.AltNames[0], requestID)
+	}
+	// AltName matched higher than SDN names, so return the SDN of the matched AltName
+	if len(search.SDNs) > 0 && len(search.AltNames) > 0 && (search.AltNames[0].Match > 0.1) && search.AltNames[0].Match > search.SDNs[0].Match {
+		alt := search.AltNames[0]
+		c.logger.Log("watchman", fmt.Sprintf("AltName=%s,SDN=%s had higher match than SDN=%s", alt.AlternateID, alt.EntityID, search.SDNs[0].EntityID), "requestID", requestID)
+		return c.altToSDN(ctx, alt, requestID)
+	}
+	// Return the SDN as Alts matched lower
+	if len(search.SDNs) > 0 {
+		return &search.SDNs[0], nil
+	}
+
+	return nil, nil // Nothing found
 }
 
 // newWatchmanClient returns an WatchmanClient instance and will default to using the Watchman address in


Test patch:
diff --git a/cmd/server/watchman_test.go b/cmd/server/watchman_test.go
index aabd2b7..8b7d2bb 100644
--- a/cmd/server/watchman_test.go
+++ b/cmd/server/watchman_test.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"math"
 	"strings"
 	"testing"
 
@@ -119,6 +120,20 @@ func TestWatchman__search(t *testing.T) {
 		t.Errorf("SDN=%s %#v", sdn.EntityID, sdn)
 	}
 
+	// Lookup an SDN from an alt
+	if c, ok := deployment.client.(*moovWatchmanClient); ok {
+		search, err := c.ofacSearch(ctx, "Osama Bin Ladin", "individual", base.ID())
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(search.SDNs) == 0 {
+			t.Fatalf("no Search results: %#v", search)
+		}
+		if sdn := search.SDNs[0]; sdn.EntityID != "6365" {
+			t.Errorf("SDN=%s %#v", sdn.EntityID, sdn)
+		}
+	}
+
 	deployment.close(t) // close only if successful
 }
 
@@ -140,3 +155,172 @@ func TestWatchman_ping(t *testing.T) {
 		}
 	}
 }
+
+func sdn(match float32) watchman.OfacSdn {
+	return watchman.OfacSdn{
+		EntityID: base.ID(),
+		Match:    match,
+	}
+}
+
+func alt(match float32) watchman.OfacAlt {
+	return watchman.OfacAlt{
+		EntityID: base.ID(),
+		Match:    match,
+	}
+}
+
+func TestWatchman__highestOfacSearchMatch(t *testing.T) {
+	search := highestOfacSearchMatch(nil)
+	if len(search.SDNs) != 0 || len(search.AltNames) != 0 {
+		t.Errorf("expected nil ofac Search, got %#v", search)
+	}
+
+	// SDN with no alts
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.80)},
+		},
+	)
+	if len(search.SDNs) != 1 || len(search.AltNames) != 0 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// Alt with no SDNs
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			AltNames: []watchman.OfacAlt{alt(0.70)},
+		},
+	)
+	if len(search.SDNs) != 0 || len(search.AltNames) != 1 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// Two SDN's, first is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.80)},
+		},
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.75)},
+		},
+	)
+	if len(search.SDNs) != 1 || math.Abs(float64(0.80-search.SDNs[0].Match)) > 0.01 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// Two SDN's, second is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.77)},
+		},
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.82)},
+		},
+	)
+	if len(search.SDNs) != 1 || math.Abs(float64(0.82-search.SDNs[0].Match)) > 0.01 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// Two Alts, first is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			AltNames: []watchman.OfacAlt{alt(0.90)},
+		},
+		&watchman.Search{
+			AltNames: []watchman.OfacAlt{alt(0.87)},
+		},
+	)
+	if len(search.AltNames) != 1 || math.Abs(float64(0.90-search.AltNames[0].Match)) > 0.01 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// Two Alts, second is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			AltNames: []watchman.OfacAlt{alt(0.87)},
+		},
+		&watchman.Search{
+			AltNames: []watchman.OfacAlt{alt(0.90)},
+		},
+	)
+	if len(search.AltNames) != 1 || math.Abs(float64(0.90-search.AltNames[0].Match)) > 0.01 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// SDN first, but other alt is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.80)},
+		},
+		&watchman.Search{
+			AltNames: []watchman.OfacAlt{alt(0.90)},
+		},
+	)
+	if len(search.SDNs) != 0 || len(search.AltNames) != 1 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// SDN first, but other alt is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.80)},
+		},
+		&watchman.Search{
+			SDNs:     []watchman.OfacSdn{sdn(0.70)},
+			AltNames: []watchman.OfacAlt{alt(0.90)},
+		},
+	)
+	if len(search.SDNs) != 1 || len(search.AltNames) != 1 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// SDN first, but other sdn is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.80)},
+		},
+		&watchman.Search{
+			SDNs:     []watchman.OfacSdn{sdn(0.99)},
+			AltNames: []watchman.OfacAlt{alt(0.90)},
+		},
+	)
+	if len(search.SDNs) != 1 || len(search.AltNames) != 1 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// SDN second and alt is higher
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs:     []watchman.OfacSdn{sdn(0.70)},
+			AltNames: []watchman.OfacAlt{alt(0.90)},
+		},
+		&watchman.Search{
+			SDNs: []watchman.OfacSdn{sdn(0.80)},
+		},
+	)
+	if len(search.SDNs) != 1 || len(search.AltNames) != 1 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+
+	// Check
+	search = highestOfacSearchMatch(
+		&watchman.Search{
+			SDNs:     []watchman.OfacSdn{sdn(0.70)},
+			AltNames: []watchman.OfacAlt{alt(0.72)},
+		},
+		&watchman.Search{
+			SDNs:     []watchman.OfacSdn{sdn(0.75)},
+			AltNames: []watchman.OfacAlt{alt(0.50)},
+		},
+	)
+	if len(search.SDNs) != 1 || len(search.AltNames) != 1 {
+		t.Fatalf("unexpected search: %#v", search)
+	}
+	if math.Abs(float64(0.75-search.SDNs[0].Match)) > 0.01 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+	if math.Abs(float64(0.50-search.AltNames[0].Match)) > 0.01 {
+		t.Errorf("unexpected search: %#v", search)
+	}
+}