Skip to content

Commit

Permalink
Merge pull request #45 from Financial-Times/feature/UPPSF-4661-migrat…
Browse files Browse the repository at this point in the history
…e-requests-to-basic-auth

migrate to basicAuth
  • Loading branch information
ManoelMilchev committed Oct 13, 2023
2 parents 121bc73 + 257e9b3 commit ad72bfe
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 41 deletions.
24 changes: 15 additions & 9 deletions content/content_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@ import (
tidutils "github.com/Financial-Times/transactionid-utils-go"
)

const apiKeyHeader = "X-Api-Key"

const syntheticContentUUID = "4f2f97ea-b8ec-11e4-b8e6-00144feab7de"
const (
syntheticContentUUID = "4f2f97ea-b8ec-11e4-b8e6-00144feab7de"
xPolicyHeader = "x-policy"
)

type API struct {
endpoint string
apiKey string
username string
password string
xPolicies []string
httpClient *http.Client
}

func NewContentAPI(endpoint string, apiKey string, httpClient *http.Client) *API {
return &API{endpoint, apiKey, httpClient}
func NewContentAPI(endpoint string, username string, password string, xPolicies []string, httpClient *http.Client) *API {
return &API{endpoint, username, password, xPolicies, httpClient}
}

func (api *API) Get(ctx context.Context, contentUUID string, log *logger.UPPLogger) (*http.Response, error) {
Expand All @@ -35,13 +38,16 @@ func (api *API) Get(ctx context.Context, contentUUID string, log *logger.UPPLogg
getContentLog = getContentLog.WithField(tidutils.TransactionIDHeader, tID)

apiReq, err := http.NewRequest("GET", apiReqURI, nil)

if err != nil {
getContentLog.WithError(err).Error("Error in creating the http request")
return nil, err
}

apiReq.Header.Set(apiKeyHeader, api.apiKey)
for _, policy := range api.xPolicies {
apiReq.Header.Add(xPolicyHeader, policy)
}

apiReq.SetBasicAuth(api.username, api.password)
if tID != "" {
apiReq.Header.Set(tidutils.TransactionIDHeader, tID)
}
Expand All @@ -57,7 +63,7 @@ func (api *API) GTG() error {
return fmt.Errorf("gtg request error: %v", err.Error())
}

apiReq.Header.Set(apiKeyHeader, api.apiKey)
apiReq.SetBasicAuth(api.username, api.password)

apiResp, err := api.httpClient.Do(apiReq)
if err != nil {
Expand Down
21 changes: 15 additions & 6 deletions content/content_api_gtg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestHappyContentAPIGTG(t *testing.T) {

testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL+"/content", testAPIKey, testClient)
cAPI := NewContentAPI(cAPIServerMock.URL+"/content", testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
assert.NoError(t, cAPI.GTG())
}

Expand All @@ -25,7 +25,7 @@ func TestUnhappyContentAPIGTG(t *testing.T) {

testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL+"/content", testAPIKey, testClient)
cAPI := NewContentAPI(cAPIServerMock.URL+"/content", testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
assert.EqualError(t, cAPI.GTG(), "gtg returned a non-200 HTTP status: 503 - I not am happy!")
}

Expand All @@ -35,14 +35,14 @@ func TestContentAPIGTGWrongAPIKey(t *testing.T) {

testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL+"/content", "a-non-existing-key", testClient)
cAPI := NewContentAPI(cAPIServerMock.URL+"/content", "a-non-existing-username", "a-non-existing-password", nil, testClient)
assert.EqualError(t, cAPI.GTG(), "gtg returned a non-200 HTTP status: 401 - unauthorized")
}

func TestContentAPIGTGInvalidURL(t *testing.T) {
testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(":#", testAPIKey, testClient)
cAPI := NewContentAPI(":#", testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
assert.Error(t, cAPI.GTG(), "Missing protocol scheme in gtg request")
}

Expand All @@ -52,19 +52,28 @@ func TestContentAPIGTGConnectionError(t *testing.T) {

testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL, testAPIKey, testClient)
cAPI := NewContentAPI(cAPIServerMock.URL, testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
assert.Error(t, cAPI.GTG())
}

func newContentAPIGTGServerMock(t *testing.T, status int, body string) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/content/"+syntheticContentUUID, r.URL.Path)
if apiKey := r.Header.Get(apiKeyHeader); apiKey != testAPIKey {
username, password, ok := r.BasicAuth()
if !ok {
w.WriteHeader(http.StatusUnauthorized)
_, err := w.Write([]byte("unauthorized"))
assert.NoError(t, err)
return
}

if username != testBasicAuthUsername || password != testBasicAuthPassword {
w.WriteHeader(http.StatusUnauthorized)
_, err := w.Write([]byte("unauthorized"))
assert.NoError(t, err)
return
}

w.WriteHeader(status)
_, err := w.Write([]byte(body))
assert.NoError(t, err)
Expand Down
1 change: 0 additions & 1 deletion content/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ func (h *Handler) WriteNativeContent(w http.ResponseWriter, r *http.Request) {
}

func (h *Handler) readContentFromUPP(ctx context.Context, w http.ResponseWriter, contentId string) {

readContentUPPLog := h.log.WithField(tidutils.TransactionIDHeader, ctx.Value(tidutils.TransactionIDHeader)).WithField("uuid", contentId)
readContentUPPLog.Warn("Draft not found in PAC, trying UPP")
uppResp, err := h.uppContentAPI.Get(ctx, contentId, h.log)
Expand Down
30 changes: 19 additions & 11 deletions content/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import (
)

const (
originIDcctTest = "cct"
contentTypeArticle = "application/vnd.ft-upp-article+json"
testAPIKey = "testAPIKey"
testTID = "test_tid"
testTimeout = 8 * time.Second
originIDcctTest = "cct"
contentTypeArticle = "application/vnd.ft-upp-article+json"
testBasicAuthUsername = "testUsername"
testBasicAuthPassword = "testPassword"
testTID = "test_tid"
testTimeout = 8 * time.Second
)

type mockDraftContentRW struct {
Expand Down Expand Up @@ -72,7 +73,7 @@ func TestReadBackOffWhenNoDraftFoundToContentAPI(t *testing.T) {
defer cAPIServerMock.Close()
testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL, testAPIKey, testClient)
cAPI := NewContentAPI(cAPIServerMock.URL, testBasicAuthUsername, testBasicAuthPassword, nil, testClient)

h := NewHandler(cAPI, rw, testTimeout, logger.NewUPPLogger("test logger", "debug"))
r := vestigo.NewRouter()
Expand Down Expand Up @@ -149,7 +150,7 @@ func TestReadNotFoundAnywhere(t *testing.T) {

testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL, testAPIKey, testClient)
cAPI := NewContentAPI(cAPIServerMock.URL, testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
h := NewHandler(cAPI, rw, testTimeout, logger.NewUPPLogger("test logger", "debug"))

r := vestigo.NewRouter()
Expand Down Expand Up @@ -178,7 +179,7 @@ func TestReadContentAPI504(t *testing.T) {

testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL, testAPIKey, testClient)
cAPI := NewContentAPI(cAPIServerMock.URL, testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
h := NewHandler(cAPI, rw, testTimeout, logger.NewUPPLogger("test logger", "debug"))
r := vestigo.NewRouter()
r.Get("/drafts/content/:uuid", h.ReadContent)
Expand All @@ -202,7 +203,7 @@ func TestReadInvalidURL(t *testing.T) {
rw.mock.On("Read", mock.Anything, mock.AnythingOfType("string")).Return(nil, ErrDraftNotFound)
testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(":#", testAPIKey, testClient)
cAPI := NewContentAPI(":#", testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
h := NewHandler(cAPI, rw, testTimeout, logger.NewUPPLogger("test logger", "debug"))
r := vestigo.NewRouter()
r.Get("/drafts/content/:uuid", h.ReadContent)
Expand All @@ -229,7 +230,7 @@ func TestReadConnectionError(t *testing.T) {

testClient, err := fthttp.NewClient(fthttp.WithSysInfo("PAC", "awesome-service"))
assert.NoError(t, err)
cAPI := NewContentAPI(cAPIServerMock.URL, testAPIKey, testClient)
cAPI := NewContentAPI(cAPIServerMock.URL, testBasicAuthUsername, testBasicAuthPassword, nil, testClient)
h := NewHandler(cAPI, rw, testTimeout, logger.NewUPPLogger("test logger", "debug"))
r := vestigo.NewRouter()
r.Get("/drafts/content/:uuid", h.ReadContent)
Expand Down Expand Up @@ -465,10 +466,17 @@ func TestWriteNativeContentWriteError(t *testing.T) {

func newContentAPIServerMock(t *testing.T, status int, body string) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if apiKey := r.Header.Get(apiKeyHeader); apiKey != testAPIKey {
username, password, ok := r.BasicAuth()
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}

if username != testBasicAuthUsername || password != testBasicAuthPassword {
w.WriteHeader(http.StatusUnauthorized)
return
}

assert.Equal(t, testTID, r.Header.Get(tidutils.TransactionIDHeader))
w.WriteHeader(status)
if _, err := w.Write([]byte(body)); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions content/handler_timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestReadTimeoutFromDraftContent(t *testing.T) {
validatorService := NewSparkDraftContentValidatorService(contentAPITestServer.server.URL, client)
resolver := NewDraftContentValidatorResolver(cctOnlyResolverConfig(validatorService))
contentRWService := NewDraftContentRWService(contentRWTestServer.server.URL, resolver, client)
uppAPI := NewContentAPI(contentAPITestServer.server.URL, "awesomely-unique-key", client)
uppAPI := NewContentAPI(contentAPITestServer.server.URL, testBasicAuthUsername, testBasicAuthPassword, nil, client)

handler := NewHandler(uppAPI, contentRWService, 150*time.Millisecond, logger.NewUPPLogger("draft-content-api-test", "debug"))

Expand Down Expand Up @@ -75,7 +75,7 @@ func TestReadTimeoutFromUPPContent(t *testing.T) {
resolver := NewDraftContentValidatorResolver(cctOnlyResolverConfig(validatorService))

contentRWService := NewDraftContentRWService(contentRWTestServer.server.URL, resolver, client)
uppAPI := NewContentAPI(contentAPITestServer.server.URL, "awesomely-unique-key", client)
uppAPI := NewContentAPI(contentAPITestServer.server.URL, testBasicAuthUsername, testBasicAuthPassword, nil, client)

handler := NewHandler(uppAPI, contentRWService, 150*time.Millisecond, logger.NewUPPLogger("draft-content-api-test", "debug"))

Expand Down Expand Up @@ -126,7 +126,7 @@ func TestNativeWriteTimeout(t *testing.T) {
resolver := NewDraftContentValidatorResolver(cctOnlyResolverConfig(validatorService))

contentRWService := NewDraftContentRWService(contentRWTestServer.server.URL, resolver, client)
uppAPI := NewContentAPI(contentAPITestServer.server.URL, "awesomely-unique-key", client)
uppAPI := NewContentAPI(contentAPITestServer.server.URL, testBasicAuthUsername, testBasicAuthPassword, nil, client)

handler := NewHandler(uppAPI, contentRWService, 150*time.Millisecond, logger.NewUPPLogger("draft-content-api-test", "debug"))

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
DRAFT_CONTENT_RW_ENDPOINT: "http://generic-rw-aurora:8080"
API_YML: "./api.yml"
VALIDATOR_YML: "./config.yml"
DELIVERY_BASIC_AUTH: "username:password"
ports:
- "8080:8080"
depends_on:
Expand Down
6 changes: 4 additions & 2 deletions helm/draft-content-api/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ spec:
configMapKeyRef:
name: global-config
key: internal-content-endpoint
- name: X_POLICIES
value: "{{ .Values.env.X_POLICIES }}"
- name: APP_TIMEOUT
valueFrom:
configMapKeyRef:
name: timeout-config
key: draft-content-api-timeout
- name: CAPI_APIKEY
- name: DELIVERY_BASIC_AUTH
valueFrom:
secretKeyRef:
name: doppler-global-secrets
key: DRAFT_CONTENT_API_UPP_API_KEY
key: UPP_DELIVERY_CLUSTER_BASIC_AUTH
- name: LOG_LEVEL
value: "{{ .Values.env.LOG_LEVEL }}"
ports:
Expand Down
1 change: 1 addition & 0 deletions helm/draft-content-api/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ resources:
memory: 128Mi
env:
LOG_LEVEL: "INFO"
X_POLICIES: "INTERNAL_UNSTABLE, INCLUDE_PROVENANCE, INCLUDE_LAST_MODIFIED_DATE, INCLUDE_RICH_CONTENT, INCLUDE_LITE"
42 changes: 33 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"net/http"
"os"
"strings"
Expand Down Expand Up @@ -62,18 +63,24 @@ func main() {
EnvVar: "DRAFT_CONTENT_RW_ENDPOINT",
})

deliveryBasicAuth := app.String(cli.StringOpt{
Name: "delivery-basic-auth",
Value: "username:password",
Desc: "Basic auth for access to the delivery UPP clusters",
EnvVar: "DELIVERY_BASIC_AUTH",
})

contentEndpoint := app.String(cli.StringOpt{
Name: "content-endpoint",
Value: "http://test.api.ft.com/content",
Value: "http://localhost:8081/content",
Desc: "Endpoint to get content from CAPI",
EnvVar: "CONTENT_ENDPOINT",
})

contentAPIKey := app.String(cli.StringOpt{
Name: "content-api-key",
Value: "",
Desc: "API key to access CAPI",
EnvVar: "CAPI_APIKEY",
xPolicies := app.Strings(cli.StringsOpt{
Name: "x-policies",
Desc: "The x-policies to apply with a request to the UPP Delivery cluster",
EnvVar: "X_POLICIES",
})

apiYml := app.String(cli.StringOpt{
Expand Down Expand Up @@ -110,10 +117,14 @@ func main() {
app.Action = func() {
log.Infof("System code: %s, App Name: %s, Port: %s, App Timeout: %sms", *appSystemCode, *appName, *port, *appTimeout)

err := validateXPolicies(*xPolicies)
if err != nil {
log.WithError(err).Fatal("invalid content policy")
}

timeout, err := time.ParseDuration(*appTimeout)
if err != nil {
log.Errorf("App could not start, error=[%s]\n", err)
return
log.Fatalf("App could not start, error=[%s]\n", err)
}

validatorConfig, err := config.ReadConfig(*validatorYml)
Expand All @@ -139,8 +150,12 @@ func main() {
draftContentRWService := content.NewDraftContentRWService(*contentRWEndpoint, resolver, httpClient)

content.AllowedContentTypes = getAllowedContentType(validatorConfig)
basicAuthCredentials := strings.Split(*deliveryBasicAuth, ":")
if len(basicAuthCredentials) != 2 {
log.Fatal("error while resolving basic auth")
}

cAPI := content.NewContentAPI(*contentEndpoint, *contentAPIKey, httpClient)
cAPI := content.NewContentAPI(*contentEndpoint, basicAuthCredentials[0], basicAuthCredentials[1], *xPolicies, httpClient)

contentHandler := content.NewHandler(cAPI, draftContentRWService, timeout, log)
healthService, err := health.NewHealthService(*appSystemCode, *appName, defaultAppDescription, draftContentRWService, cAPI,
Expand All @@ -158,6 +173,15 @@ func main() {
}
}

func validateXPolicies(policies []string) error {
for _, policy := range policies {
if strings.ContainsAny(policy, " ;") {
return fmt.Errorf("policies should not contain spaces or semicolons: '%s'", policy)
}
}
return nil
}

func extractServices(dcm map[string]content.DraftContentValidator) []health.ExternalService {
result := make([]health.ExternalService, 0, len(dcm))

Expand Down

0 comments on commit ad72bfe

Please sign in to comment.