Repository: moov-io/customers (go)

Issue:
documents: inner join on customers.namespace 
Customers Version: `master`

Currently we [do not check `X-Namespace` on documents requests](https://github.com/moov-io/customers/blob/3127cb6a8ac1fe17ed62f41bf6b5d2f372b51cbc/pkg/customers/documents.go#L86) and it was left off of doing so. We should `inner join` on `customers.namespace` to verify this from the request. 

@alovak brought this up https://github.com/moov-io/customers/pull/184#discussion_r493892791 

Pull request:
documents: check namespace match on customers when fetching documents
Performs a check against `customers.namespace` when fetching customer documents.

Fixes #190

Patch:
diff --git a/pkg/customers/customers.go b/pkg/customers/customers.go
index ff2fa1a..e598584 100644
--- a/pkg/customers/customers.go
+++ b/pkg/customers/customers.go
@@ -452,13 +452,8 @@ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
 	}
 	defer stmt.Close()
 
-	var birthDate *string
-	if c.BirthDate != "" {
-		birthDate = &c.BirthDate
-	}
-
 	now := time.Now()
-	_, err = stmt.Exec(c.CustomerID, c.FirstName, c.MiddleName, c.LastName, c.NickName, c.Suffix, c.Type, birthDate, client.UNKNOWN, c.Email, now, now, namespace)
+	_, err = stmt.Exec(c.CustomerID, c.FirstName, c.MiddleName, c.LastName, c.NickName, c.Suffix, c.Type, c.BirthDate, client.UNKNOWN, c.Email, now, now, namespace)
 	if err != nil {
 		return fmt.Errorf("createCustomer: insert into customers err=%v | rollback=%v", err, tx.Rollback())
 	}
@@ -571,10 +566,10 @@ func (r *sqlCustomerRepository) getCustomer(customerID string) (*client.Customer
 	row := stmt.QueryRow(customerID)
 
 	var birthDate *string
+
 	var cust client.Customer
 	cust.CustomerID = customerID
-	err = row.Scan(&cust.FirstName, &cust.MiddleName, &cust.LastName, &cust.NickName, &cust.Suffix, &cust.Type, &birthDate, &cust.Status, &cust.Email, &cust.CreatedAt,
-		&cust.LastModified)
+	err = row.Scan(&cust.FirstName, &cust.MiddleName, &cust.LastName, &cust.NickName, &cust.Suffix, &cust.Type, &birthDate, &cust.Status, &cust.Email, &cust.CreatedAt, &cust.LastModified)
 	stmt.Close()
 	if err != nil && !strings.Contains(err.Error(), "no rows in result set") {
 		return nil, fmt.Errorf("getCustomer: %v", err)
diff --git a/pkg/documents/disclaimers.go b/pkg/documents/disclaimers.go
index 3b660c8..1b05ab6 100644
--- a/pkg/documents/disclaimers.go
+++ b/pkg/documents/disclaimers.go
@@ -15,6 +15,7 @@ import (
 	"github.com/moov-io/base"
 	"github.com/moov-io/base/admin"
 	moovhttp "github.com/moov-io/base/http"
+
 	"github.com/moov-io/customers/pkg/client"
 	"github.com/moov-io/customers/pkg/route"
 
@@ -106,8 +107,8 @@ func createDisclaimer(logger log.Logger, disclaimerRepo DisclaimerRepository, do
 			return
 		}
 
-		customerID := route.GetCustomerID(w, r)
-		if customerID == "" {
+		customerID, namespace := route.GetCustomerID(w, r), route.GetNamespace(w, r)
+		if customerID == "" || namespace == "" {
 			return
 		}
 
@@ -117,7 +118,7 @@ func createDisclaimer(logger log.Logger, disclaimerRepo DisclaimerRepository, do
 			return
 		}
 
-		if err := documentExistsForCustomer(customerID, req, docRepo); err != nil {
+		if err := documentExistsForCustomer(customerID, namespace, req, docRepo); err != nil {
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -138,9 +139,9 @@ func createDisclaimer(logger log.Logger, disclaimerRepo DisclaimerRepository, do
 	}
 }
 
-func documentExistsForCustomer(customerID string, req createDisclaimerRequest, docRepo DocumentRepository) error {
+func documentExistsForCustomer(customerID string, namespace string, req createDisclaimerRequest, docRepo DocumentRepository) error {
 	if req.DocumentID != "" {
-		docs, err := docRepo.getCustomerDocuments(customerID)
+		docs, err := docRepo.getCustomerDocuments(customerID, namespace)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/documents/documents.go b/pkg/documents/documents.go
index 1b65fa8..4de17ed 100644
--- a/pkg/documents/documents.go
+++ b/pkg/documents/documents.go
@@ -54,12 +54,12 @@ func getCustomerDocuments(logger log.Logger, repo DocumentRepository) http.Handl
 	return func(w http.ResponseWriter, r *http.Request) {
 		w = route.Responder(logger, w, r)
 
-		customerID := route.GetCustomerID(w, r)
-		if customerID == "" {
+		customerID, namespace := route.GetCustomerID(w, r), route.GetNamespace(w, r)
+		if customerID == "" || namespace == "" {
 			return
 		}
 
-		docs, err := repo.getCustomerDocuments(customerID)
+		docs, err := repo.getCustomerDocuments(customerID, namespace)
 		if err != nil {
 			moovhttp.Problem(w, err)
 			return
@@ -230,7 +230,7 @@ func makeDocumentKey(customerID, documentId string) string {
 }
 
 type DocumentRepository interface {
-	getCustomerDocuments(customerID string) ([]*client.Document, error)
+	getCustomerDocuments(customerID string, namespace string) ([]*client.Document, error)
 	writeCustomerDocument(customerID string, doc *client.Document) error
 	deleteCustomerDocument(customerID string, documentID string) error
 }
@@ -246,15 +246,16 @@ func NewDocumentRepo(logger log.Logger, db *sql.DB) DocumentRepository {
 		logger: logger,
 	}
 }
-func (r *sqlDocumentRepository) getCustomerDocuments(customerID string) ([]*client.Document, error) {
-	query := `select document_id, type, content_type, uploaded_at from documents where customer_id = ? and deleted_at is null`
+func (r *sqlDocumentRepository) getCustomerDocuments(customerID string, namespace string) ([]*client.Document, error) {
+	query := `select document_id, documents.type, content_type, uploaded_at from documents join customers on customers.
+	namespace = ? where documents.customer_id = ? and documents.deleted_at is null`
 	stmt, err := r.db.Prepare(query)
 	if err != nil {
 		return nil, fmt.Errorf("getCustomerDocuments: prepare %v", err)
 	}
 	defer stmt.Close()
 
-	rows, err := stmt.Query(customerID)
+	rows, err := stmt.Query(namespace, customerID)
 	if err != nil {
 		return nil, fmt.Errorf("getCustomerDocuments: query %v", err)
 	}


Test patch:
diff --git a/pkg/documents/documents_test.go b/pkg/documents/documents_test.go
index 48143f8..d246907 100644
--- a/pkg/documents/documents_test.go
+++ b/pkg/documents/documents_test.go
@@ -6,6 +6,7 @@ package documents
 
 import (
 	"bytes"
+	"database/sql"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -25,7 +26,10 @@ import (
 
 	"github.com/moov-io/customers/internal/database"
 	"github.com/moov-io/customers/pkg/client"
+	"github.com/moov-io/customers/pkg/customers"
 	"github.com/moov-io/customers/pkg/documents/storage"
+	"github.com/moov-io/customers/pkg/secrets"
+	"github.com/moov-io/customers/pkg/watchman"
 
 	"github.com/go-kit/kit/log"
 	"github.com/gorilla/mux"
@@ -38,7 +42,7 @@ type testDocumentRepository struct {
 	written *client.Document
 }
 
-func (r *testDocumentRepository) getCustomerDocuments(customerID string) ([]*client.Document, error) {
+func (r *testDocumentRepository) getCustomerDocuments(customerID string, namespace string) ([]*client.Document, error) {
 	if r.err != nil {
 		return nil, r.err
 	}
@@ -72,6 +76,7 @@ func TestDocuments__getCustomerDocuments(t *testing.T) {
 	w := httptest.NewRecorder()
 	req := httptest.NewRequest("GET", "/customers/foo/documents", nil)
 	req.Header.Set("x-request-id", "test")
+	req.Header.Set("namespace", "test")
 
 	router := mux.NewRouter()
 	AddDocumentRoutes(log.NewNopLogger(), router, repo, storage.TestBucket)
@@ -155,6 +160,7 @@ func TestDocumentsUploadAndRetrieval(t *testing.T) {
 	w := httptest.NewRecorder()
 	req := multipartRequest(t)
 	req.Header.Set("x-request-id", "test")
+	req.Header.Set("X-namespace", "test")
 
 	router := mux.NewRouter()
 	AddDocumentRoutes(log.NewNopLogger(), router, repo, storage.TestBucket)
@@ -214,9 +220,11 @@ func TestDocuments__delete(t *testing.T) {
 
 	require.Equal(t, http.StatusNoContent, w.Code)
 
-	docs, err := repo.getCustomerDocuments(customerID)
-	require.NoError(t, err)
-	require.Empty(t, docs)
+	var count int
+	row := db.DB.QueryRow("SELECT COUNT(*) FROM documents where deleted_at is null")
+	require.NoError(t, row.Scan(&count))
+	require.Equal(t, 0, count)
+
 }
 
 func TestDocuments__uploadCustomerDocument(t *testing.T) {
@@ -249,50 +257,65 @@ func TestDocuments__makeDocumentKey(t *testing.T) {
 }
 
 func TestDocumentRepository(t *testing.T) {
-	customerID := base.ID()
-
-	check := func(t *testing.T, repo *sqlDocumentRepository) {
-		docs, err := repo.getCustomerDocuments(customerID)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if len(docs) != 0 {
-			t.Errorf("got %d unexpected documents: %#v", len(docs), docs)
-		}
-
-		// Write a Document and read it back
-		doc := &client.Document{
-			DocumentID:  base.ID(),
-			Type:        "DriversLicense",
-			ContentType: "image/png",
-		}
-		if err := repo.writeCustomerDocument(customerID, doc); err != nil {
-			t.Fatal(err)
-		}
-		docs, err = repo.getCustomerDocuments(customerID)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if len(docs) != 1 {
-			t.Errorf("got %d unexpected documents: %#v", len(docs), docs)
-		}
-		if docs[0].DocumentID != doc.DocumentID {
-			t.Errorf("got docs[0].DocumentID=%q / want doc.DocumentID=%q", docs[0].DocumentID, doc.DocumentID)
-		}
-		if docs[0].ContentType != "image/png" {
-			t.Errorf("docs[0].ContentType=%q", docs[0].ContentType)
-		}
+	tests := []struct {
+		dbName string
+		db     *sql.DB
+	}{
+		{
+			dbName: "sqlite",
+			db:     database.CreateTestSqliteDB(t).DB,
+		},
+		{
+			dbName: "mysql",
+			db:     database.CreateTestMySQLDB(t).DB,
+		},
 	}
 
-	// SQLite tests
-	sqliteDB := database.CreateTestSqliteDB(t)
-	defer sqliteDB.Close()
-	check(t, &sqlDocumentRepository{sqliteDB.DB, log.NewNopLogger()})
-
-	// MySQL tests
-	mysqlDB := database.CreateTestMySQLDB(t)
-	defer mysqlDB.Close()
-	check(t, &sqlDocumentRepository{mysqlDB.DB, log.NewNopLogger()})
+	for _, tc := range tests {
+		defer tc.db.Close()
+
+		t.Run(tc.dbName, func(t *testing.T) {
+			logger := log.NewNopLogger()
+			documentRepo := NewDocumentRepo(logger, tc.db)
+			customerRepo := customers.NewCustomerRepo(logger, tc.db)
+			namespace := "test"
+			customerID := base.ID()
+
+			// check empty docs
+			docs, err := documentRepo.getCustomerDocuments(customerID, namespace)
+			require.NoError(t, err)
+			require.Empty(t, docs)
+
+			// create test customer with namespace
+			router := mux.NewRouter()
+			ssnStorage := customers.NewSSNStorage(secrets.TestStringKeeper(t), customers.NewCustomerSSNRepository(logger, tc.db))
+			ofacSearcher := customers.NewOFACSearcher(customerRepo, &watchman.TestWatchmanClient{})
+			customers.AddCustomerRoutes(log.NewNopLogger(), router, customerRepo, ssnStorage, ofacSearcher)
+			body := `{"firstName": "jane", "lastName": "doe", "email": "jane@example.com", "birthDate": "1991-04-01", "ssn": "123456789", "type": "individual"}`
+			req := httptest.NewRequest("POST", "/customers", strings.NewReader(body))
+
+			req.Header.Add("X-Namespace", namespace)
+			res := httptest.NewRecorder()
+			router.ServeHTTP(res, req)
+			require.Equal(t, http.StatusOK, res.Code)
+
+			// Write a Document and read it back
+			doc := &client.Document{
+				DocumentID:  base.ID(),
+				Type:        "DriversLicense",
+				ContentType: "image/png",
+			}
+			if err := documentRepo.writeCustomerDocument(customerID, doc); err != nil {
+				t.Fatal(err)
+			}
+			docs, err = documentRepo.getCustomerDocuments(customerID, namespace)
+			require.NoError(t, err)
+			require.Len(t, docs, 1)
+
+			require.Equal(t, doc.DocumentID, docs[0].DocumentID)
+			require.Equal(t, "image/png", docs[0].ContentType)
+		})
+	}
 }
 
 func TestDocumentsRepository__Delete(t *testing.T) {
@@ -331,7 +354,7 @@ func TestDocumentsRepository__Delete(t *testing.T) {
 
 	deletedDocIDs := make(map[string]bool)
 	// query all documents that have been marked as deleted
-	query := `select document_id from documents where deleted_at is not null;`
+	query := ` select document_id from documents where deleted_at is not null `
 	stmt, err := repo.db.Prepare(query)
 	require.NoError(t, err)