Skip to content

Commit

Permalink
Added Basic and Bearer authentication when self-hosting Open Sanctions.
Browse files Browse the repository at this point in the history
  • Loading branch information
Antoine Popineau authored and apognu committed Jan 31, 2025
1 parent 2a72914 commit 03b3d7a
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 22 deletions.
2 changes: 2 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ func RunServer() error {
ProjectID: utils.GetEnv("CONVOY_PROJECT_ID", ""),
RateLimit: utils.GetEnv("CONVOY_RATE_LIMIT", 50),
}

openSanctionsConfig := infra.InitializeOpenSanctions(
http.DefaultClient,
utils.GetEnv("OPENSANCTIONS_API_HOST", ""),
utils.GetEnv("OPENSANCTIONS_AUTH_METHOD", ""),
utils.GetEnv("OPENSANCTIONS_API_KEY", ""),
)

Expand Down
46 changes: 34 additions & 12 deletions infra/opensanctions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,38 @@ const (
OPEN_SANCTIONS_API_HOST = "https://api.opensanctions.org"
)

type OpenSanctionsAuthMethod int

const (
OPEN_SANCTIONS_AUTH_SAAS OpenSanctionsAuthMethod = iota
OPEN_SANCTIONS_AUTH_BEARER
OPEN_SANCTIONS_AUTH_BASIC
)

type OpenSanctions struct {
client *http.Client
host string
// TODO: this is only for SaaS OpenSanctions API, we may need to abstract
// over authentication to at least offer Basic and Bearer for self-hosted.
apiKey string
client *http.Client
host string
authMethod OpenSanctionsAuthMethod
credentials string
}

func InitializeOpenSanctions(client *http.Client, host, apiKey string) OpenSanctions {
return OpenSanctions{
client: client,
host: host,
apiKey: apiKey,
func InitializeOpenSanctions(client *http.Client, host, authMethod, creds string) OpenSanctions {
os := OpenSanctions{
client: client,
host: host,
credentials: creds,
}

if os.IsSelfHosted() {
switch authMethod {
case "bearer":
os.authMethod = OPEN_SANCTIONS_AUTH_BEARER
case "basic":
os.authMethod = OPEN_SANCTIONS_AUTH_BASIC
}
}

return os
}

func (os OpenSanctions) Client() *http.Client {
Expand All @@ -38,6 +56,10 @@ func (os OpenSanctions) Host() string {
return OPEN_SANCTIONS_API_HOST
}

func (os OpenSanctions) ApiKey() string {
return os.apiKey
func (os OpenSanctions) AuthMethod() OpenSanctionsAuthMethod {
return os.authMethod
}

func (os OpenSanctions) Credentials() string {
return os.credentials
}
17 changes: 15 additions & 2 deletions repositories/opensanctions_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/checkmarble/marble-backend/infra"
Expand Down Expand Up @@ -165,14 +166,26 @@ func (repo OpenSanctionsRepository) searchRequest(ctx context.Context,

req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestUrl, &body)

if repo.opensanctions.IsSelfHosted() {
switch repo.opensanctions.AuthMethod() {
case infra.OPEN_SANCTIONS_AUTH_BEARER:
req.Header.Set("authorization", "Bearer "+repo.opensanctions.Credentials())
case infra.OPEN_SANCTIONS_AUTH_BASIC:
u, p, _ := strings.Cut(repo.opensanctions.Credentials(), ":")

req.SetBasicAuth(u, p)
}
}

return req, rawQuery.Bytes(), err
}

func (repo OpenSanctionsRepository) buildQueryString(cfg models.SanctionCheckConfig, orgCfg models.OrganizationOpenSanctionsConfig) url.Values {
qs := url.Values{}

if len(repo.opensanctions.ApiKey()) > 0 {
qs.Set("api_key", repo.opensanctions.ApiKey())
if repo.opensanctions.AuthMethod() == infra.OPEN_SANCTIONS_AUTH_SAAS &&
len(repo.opensanctions.Credentials()) > 0 {
qs.Set("api_key", repo.opensanctions.Credentials())
}

if len(cfg.Datasets) > 0 {
Expand Down
62 changes: 54 additions & 8 deletions repositories/opensanctions_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ import (
"github.com/stretchr/testify/assert"
)

func getMockedOpenSanctionsRepository(host, apiKey string) OpenSanctionsRepository {
func getMockedOpenSanctionsRepository(host, authMethod, apiKey string) OpenSanctionsRepository {
client := &http.Client{Transport: &http.Transport{}}

gock.InterceptClient(client)

return OpenSanctionsRepository{
opensanctions: infra.InitializeOpenSanctions(client, host, apiKey),
opensanctions: infra.InitializeOpenSanctions(client, host, authMethod, apiKey),
}
}

func TestOpenSanctionsSelfHostedApi(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("https://yente.local", "")
repo := getMockedOpenSanctionsRepository("https://yente.local", "", "")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
Expand All @@ -49,7 +49,7 @@ func TestOpenSanctionsSelfHostedApi(t *testing.T) {
func TestOpenSanctionsSelfHostedAndApiKey(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("https://yente.local", "abcdef")
repo := getMockedOpenSanctionsRepository("https://yente.local", "", "abcdef")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
Expand All @@ -72,7 +72,7 @@ func TestOpenSanctionsSelfHostedAndApiKey(t *testing.T) {
func TestOpenSanctionsSaaSAndApiKey(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("", "abcdef")
repo := getMockedOpenSanctionsRepository("", "", "abcdef")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
Expand All @@ -92,10 +92,56 @@ func TestOpenSanctionsSaaSAndApiKey(t *testing.T) {
assert.Error(t, err)
}

func TestOpenSanctionsSelfHostedAndBearerToken(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("https://yente.local", "bearer", "abcdef")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
"name": []string{"bob"},
},
OrgConfig: models.OrganizationOpenSanctionsConfig{},
}

gock.New("https://yente.local").
Post("/match/sanctions").
MatchHeader("authorization", "Bearer abcdef").
Reply(http.StatusBadRequest)

_, err := repo.Search(context.TODO(), cfg, query)

assert.False(t, gock.HasUnmatchedRequest())
assert.Error(t, err)
}

func TestOpenSanctionsSelfHostedAndBasicAuth(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("https://yente.local", "basic", "abcdef:helloworld")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
"name": []string{"bob"},
},
OrgConfig: models.OrganizationOpenSanctionsConfig{},
}

gock.New("https://yente.local").
Post("/match/sanctions").
MatchHeader("authorization", "Basic YWJjZGVmOmhlbGxvd29ybGQ=").
Reply(http.StatusBadRequest)

_, err := repo.Search(context.TODO(), cfg, query)

assert.False(t, gock.HasUnmatchedRequest())
assert.Error(t, err)
}

func TestOpenSanctionsError(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("", "")
repo := getMockedOpenSanctionsRepository("", "", "")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
Expand All @@ -117,7 +163,7 @@ func TestOpenSanctionsError(t *testing.T) {
func TestOpenSanctionsSuccessfulPartialResponse(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("", "")
repo := getMockedOpenSanctionsRepository("", "", "")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
Expand Down Expand Up @@ -145,7 +191,7 @@ func TestOpenSanctionsSuccessfulPartialResponse(t *testing.T) {
func TestOpenSanctionsSuccessfulFullResponse(t *testing.T) {
defer gock.Off()

repo := getMockedOpenSanctionsRepository("", "")
repo := getMockedOpenSanctionsRepository("", "", "")
cfg := models.SanctionCheckConfig{}
query := models.OpenSanctionsQuery{
Queries: models.OpenSanctionCheckFilter{
Expand Down

0 comments on commit 03b3d7a

Please sign in to comment.