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
69 changes: 65 additions & 4 deletions cla-backend-go/v2/member-service/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,71 @@ func NewClient(cfg Config) (*Client, error) {
return &Client{cfg: cfg, httpClient: &http.Client{Timeout: 30 * time.Second}}, nil
}

const sfidSuffixChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"

// sfid18 returns the canonical 18-character form of a 15- or 18-character Salesforce ID; the
// gateway matches the path id against b2b_org:<18-char> FGA tuples, so a 15-character id is
// always refused.
func sfid18(id string) (string, bool) {
id = strings.TrimSpace(id)
if len(id) != 15 && len(id) != 18 {
return "", false
}
var b [18]byte
copy(b[:], id[:15])
for _, c := range b[:15] {
if !(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9') {
return "", false
}
}
if len(id) == 18 && !restoreSFIDCase(b[:15], id[15:]) {
return "", false
}
for g := 0; g < 3; g++ {
bits := 0
for j := 0; j < 5; j++ {
if c := b[g*5+j]; c >= 'A' && c <= 'Z' {
bits |= 1 << j
}
}
b[15+g] = sfidSuffixChars[bits]
}
return string(b[:]), true
}
Comment thread
lukaszgryglicki marked this conversation as resolved.

// restoreSFIDCase re-applies the letter case encoded by the case-insensitive 3-character suffix
// (bit j of suffix character g set <=> position g*5+j is an uppercase letter).
func restoreSFIDCase(id []byte, suffix string) bool {
for g := 0; g < 3; g++ {
s := suffix[g]
if s >= 'a' && s <= 'z' {
s -= 'a' - 'A'
}
bits := strings.IndexByte(sfidSuffixChars, s)
if bits < 0 {
return false
}
for j := 0; j < 5; j++ {
c := &id[g*5+j]
switch {
case bits&(1<<j) == 0:
if *c >= 'A' && *c <= 'Z' {
*c += 'a' - 'A'
}
case *c >= 'a' && *c <= 'z':
*c -= 'a' - 'A'
case *c < 'A' || *c > 'Z':
return false
}
}
}
return true
}

// GetB2BOrg returns the registered B2B org (dry-run liveness check; member-service reads Salesforce).
func (c *Client) GetB2BOrg(ctx context.Context, uid string) (*B2BOrg, error) {
uid = strings.TrimSpace(uid)
if len(uid) != 15 && len(uid) != 18 {
uid, ok := sfid18(uid)
if !ok {
return nil, ErrInvalidSFID
}
tok, err := c.getToken(ctx)
Expand All @@ -109,8 +170,8 @@ func (c *Client) GetB2BOrg(ctx context.Context, uid string) (*B2BOrg, error) {
// RegisterB2BOrg registers the Salesforce account as a B2B org (idempotent on the member-service
// side) and returns the resulting record.
func (c *Client) RegisterB2BOrg(ctx context.Context, sfid string) (*B2BOrg, error) {
sfid = strings.TrimSpace(sfid)
if len(sfid) != 15 && len(sfid) != 18 {
sfid, ok := sfid18(sfid)
if !ok {
return nil, ErrInvalidSFID
}
tok, err := c.getToken(ctx)
Expand Down
74 changes: 73 additions & 1 deletion cla-backend-go/v2/member-service/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import (
"github.com/stretchr/testify/require"
)

const syntheticSFID18 = "001Ab00000CdEfGIAV"

type fakeMemberService struct {
t *testing.T
tokenCalls int
getCalls int
getPaths []string
registerBody []map[string]string
status int
response interface{}
Expand All @@ -44,11 +47,12 @@ func (f *fakeMemberService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
assert.Equal(f.t, "client_credentials", req["grant_type"])
assert.Equal(f.t, "https://member.example/", req["audience"])
f.encode(w, map[string]interface{}{"access_token": "member-token", "token_type": "Bearer", "expires_in": 3600})
case "/b2b_orgs/0014100000Te0G7AAJ":
case "/b2b_orgs/0014100000Te0G7AAJ", "/b2b_orgs/" + syntheticSFID18:
assert.Equal(f.t, http.MethodGet, r.Method)
assert.Equal(f.t, "Bearer member-token", r.Header.Get("Authorization"))
assert.Equal(f.t, "1", r.URL.Query().Get("v"))
f.getCalls++
f.getPaths = append(f.getPaths, r.URL.Path)
w.WriteHeader(f.status)
if f.response != nil {
f.encode(w, f.response)
Expand Down Expand Up @@ -114,13 +118,81 @@ func TestRegisterB2BOrg(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "Infosys Limited", org.Name)
assert.Equal(t, 1, fake.getCalls)
org, err = client.GetB2BOrg(context.Background(), " 0014100000Te0G7 ")
require.NoError(t, err, "15-char ids are sent in the 18-char form the gateway matches tuples on")
assert.Equal(t, "Infosys Limited", org.Name)
assert.Equal(t, 2, fake.getCalls)
fake.status = http.StatusCreated
_, err = client.RegisterB2BOrg(context.Background(), "0014100000Te0G7")
require.NoError(t, err)
assert.Equal(t, map[string]string{"sfid": "0014100000Te0G7AAJ"}, fake.registerBody[len(fake.registerBody)-1])
fake.status = http.StatusNotFound
_, err = client.GetB2BOrg(context.Background(), "0014100000Te0G7AAJ")
assert.ErrorIs(t, err, ErrOrgNotFound)
_, err = client.GetB2BOrg(context.Background(), "lf-not-an-sfid")
assert.ErrorIs(t, err, ErrInvalidSFID)
}

func TestSFID18(t *testing.T) {
// real Account id pairs from the dev companies table
for in, want := range map[string]string{
"0014100000Te0Rk": "0014100000Te0RkAAJ",
"0012h00000hFI9F": "0012h00000hFI9FAAW",
"0014100000Te0G7": "0014100000Te0G7AAJ",
"0012M00002VjHnZ": "0012M00002VjHnZQAV",
"0012M00002p9y2q": "0012M00002p9y2qQAA",
"0014100000Te0yq": "0014100000Te0yqAAB",
"0014100000Te0RkAAJ": "0014100000Te0RkAAJ",
"0014100000Te0Rkaaj": "0014100000Te0RkAAJ",
" 0012M00002VjHnZ\n": "0012M00002VjHnZQAV",
// synthetic pair: the suffix restores the letter case of any case-folded 18-char form
"001Ab00000CdEfG": syntheticSFID18,
syntheticSFID18: syntheticSFID18,
"001ab00000cdefgiav": syntheticSFID18,
"001AB00000CDEFGIAV": syntheticSFID18,
"001aB00000cDeFgIaV": syntheticSFID18,
"001Ab00000CdEfGiav": syntheticSFID18,
"001ab00000cdefg": "001ab00000cdefgAAA",
} {
got, ok := sfid18(in)
assert.True(t, ok, in)
assert.Equal(t, want, got, in)
}
// suffix outside A-Z/0-5, or an uppercase bit on a digit position, is malformed
for _, in := range []string{"", "0014100000Te0R", "0014100000Te0RkA", "0014100000Te0RkAAJX", "0014100000Te0R-", "lf-not-an-sfid-xxx",
"001Ab00000CdEfGIA6", "001Ab00000CdEfGIA-", "001Ab00000CdEfG-AV", "001Ab00000CdEfGJAV", "001Ab00000CdEfGIBV"} {
got, ok := sfid18(in)
assert.False(t, ok, in)
assert.Empty(t, got, in)
}
}

func TestB2BOrgCaseFoldedSFID(t *testing.T) {
fake := &fakeMemberService{status: http.StatusOK, response: map[string]string{"uid": syntheticSFID18, "name": "Synthetic Org"}}
client := newTestClient(t, fake)

org, err := client.GetB2BOrg(context.Background(), "001ab00000cdefgiav")
require.NoError(t, err)
assert.Equal(t, syntheticSFID18, org.UID)
assert.Equal(t, []string{"/b2b_orgs/" + syntheticSFID18}, fake.getPaths, "GET path carries the case-restored canonical id")

fake.status = http.StatusCreated
_, err = client.RegisterB2BOrg(context.Background(), "001AB00000CDEFGIAV")
require.NoError(t, err)
assert.Equal(t, []map[string]string{{"sfid": syntheticSFID18}}, fake.registerBody, "POST payload carries the case-restored canonical id")

client = newTestClient(t, fake)
for _, in := range []string{"001Ab00000CdEfGJAV", "001Ab00000CdEfGIA6"} {
_, err = client.GetB2BOrg(context.Background(), in)
assert.ErrorIs(t, err, ErrInvalidSFID, in)
_, err = client.RegisterB2BOrg(context.Background(), in)
assert.ErrorIs(t, err, ErrInvalidSFID, in)
}
assert.Equal(t, 1, fake.getCalls, "malformed suffixes never reach the service")
assert.Len(t, fake.registerBody, 1)
assert.Equal(t, 1, fake.tokenCalls, "malformed suffixes are refused before a token is minted")
}

func TestRegisterB2BOrgErrors(t *testing.T) {
fake := &fakeMemberService{status: http.StatusNotFound, response: map[string]string{"name": "NotFound", "message": "b2b org not found"}}
client := newTestClient(t, fake)
Expand Down
Loading
Loading