Skip to content

Commit

Permalink
Merge 98bdfd8 into 558bd9a
Browse files Browse the repository at this point in the history
  • Loading branch information
acsauk authored Oct 24, 2023
2 parents 558bd9a + 98bdfd8 commit 2bd35e6
Show file tree
Hide file tree
Showing 24 changed files with 663 additions and 343 deletions.
2 changes: 1 addition & 1 deletion .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = "."
tmp_dir = "tmp"

[build]
exclude_dir = ["cypress", "docs", "tmp", "web/assets", "web/static", "node_modules", "scripts", "terraform"]
exclude_dir = ["cypress", "docs", "tmp", "web/assets", "web/static", "node_modules", "scripts", "terraform", "cmd/event-received", "cmd/enumerator", "cmd/event-mock-notify", "cmd/mock.onelogin", "cmd/mock-os-api"]
cmd = "cd cmd/mlpa && go build -ldflags='-X main.Tag=${TAG}' -gcflags='all=-N -l' -o /tmp/mlpa ."
full_bin = "pkill -9 'dlv|mlpa'; sleep 0.1; dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 /tmp/mlpa"
include_ext = ["go", "gohtml", "json"]
36 changes: 24 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,37 @@ run-structurizr-export:
scan-lpas: ##@app dumps all entries in the lpas dynamodb table
docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb scan --table-name lpas

get-lpa: ##@app dumps all entries in the lpas dynamodb table that are related to the LPA id supplied e.g. get-lpa ID=abc-123
get-lpa: ##@app dumps all entries in the lpas dynamodb table that are related to the LPA id supplied e.g. get-lpa id=abc-123
docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb \
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(ID)"}}'
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(id)"}}'

get-evidence: ##@app dumps all fee evidence in the lpas dynamodb table that are related to the LPA id supplied e.g. get-evidence ID=abc-123
get-evidence: ##@app dumps all fee evidence in the lpas dynamodb table that are related to the LPA id supplied e.g. get-evidence id=abc-123
docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb \
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(ID)"}}' --projection-expression "EvidenceKeys"
query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(id)"}}' --projection-expression "Evidence"

emit-evidence-received: ##@app emits an evidence-received event with the given UID e.g. emit-evidence-received UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"evidence-received","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-evidence-received: ##@app emits an evidence-received event with the given UID e.g. emit-evidence-received uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"evidence-received","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-fee-approved: ##@app emits a fee-approved event with the given UID e.g. emit-fee-approved UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-approved","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-fee-approved: ##@app emits a fee-approved event with the given UID e.g. emit-fee-approved uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-approved","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-fee-denied: ##@app emits a fee-denied event with the given UID e.g. emit-fee-denied UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-denied","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-fee-denied: ##@app emits a fee-denied event with the given UID e.g. emit-fee-denied uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"fee-denied","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-more-evidence-required: ##@app emits a more-evidence-required event with the given UID e.g. emit-more-evidence-required UID=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"more-evidence-required","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(UID)"}}'
emit-more-evidence-required: ##@app emits a more-evidence-required event with the given UID e.g. emit-more-evidence-required uid=abc-123
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"more-evidence-required","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"UID":"$(uid)"}}'

emit-object-tags-added-with-virus: ##@app emits a Object Tags Added event with the given S3 key e.g. emit-object-tags-added-with-virus key=doc/key. Also ensures a tag with virus-scan-status exists on an existing object set to infected
docker compose -f docker/docker-compose.yml exec localstack awslocal s3api \
put-object-tagging --bucket evidence --key $(key) --tagging '{"TagSet": [{ "Key": "virus-scan-status", "Value": "infected" }]}'

curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"Object Tags Added","source":"aws.s3","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"object":{"key":"$(key)"}}}'

emit-object-tags-added-without-virus: ##@app emits a Object Tags Added event with the given S3 key e.g. emit-object-tags-added-with-virus key=doc/key. Also ensures a tag with virus-scan-status exists on an existing object set to ok
docker compose -f docker/docker-compose.yml exec localstack awslocal s3api \
put-object-tagging --bucket evidence --key $(key) --tagging '{"TagSet": [{ "Key": "virus-scan-status", "Value": "ok" }]}'

curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"Object Tags Added","source":"aws.s3","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"object":{"key":"$(key)"}}}'

logs: ##@app tails logs for all containers running
docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml logs -f
193 changes: 140 additions & 53 deletions cmd/event-received/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/ministryofjustice/opg-modernising-lpa/internal/actor"
"github.com/ministryofjustice/opg-modernising-lpa/internal/app"
"github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo"
"github.com/ministryofjustice/opg-modernising-lpa/internal/localize"
"github.com/ministryofjustice/opg-modernising-lpa/internal/notify"
"github.com/ministryofjustice/opg-modernising-lpa/internal/page"
"github.com/ministryofjustice/opg-modernising-lpa/internal/random"
"github.com/ministryofjustice/opg-modernising-lpa/internal/s3"
"github.com/ministryofjustice/opg-modernising-lpa/internal/secrets"
)

const (
virusFound = "infected"
evidenceReceivedEventName = "evidence-received"
feeApprovedEventName = "fee-approved"
feeDeniedEventName = "fee-denied"
moreEvidenceRequiredEventName = "more-evidence-required"
objectTagsAddedEventName = "ObjectTagging:Put"
)

type uidEvent struct {
UID string `json:"uid"`
}
Expand All @@ -34,17 +45,36 @@ type dynamodbClient interface {
Put(ctx context.Context, v interface{}) error
}

//go:generate mockery --testonly --inpackage --name s3Client --structname mockS3Client
type s3Client interface {
GetObjectTags(ctx context.Context, key string) ([]types.Tag, error)
}

//go:generate mockery --testonly --inpackage --name shareCodeSender --structname mockShareCodeSender
type shareCodeSender interface {
SendCertificateProvider(context.Context, notify.Template, page.AppData, bool, *page.Lpa) error
}

func Handler(ctx context.Context, event events.CloudWatchEvent) error {
type Event struct {
events.S3Event
events.CloudWatchEvent
}

func (e Event) isS3Event() bool {
return len(e.Records) > 0
}

func (e Event) isCloudWatchEvent() bool {
return e.Source == "aws.cloudwatch"
}

func Handler(ctx context.Context, event Event) error {
tableName := os.Getenv("LPAS_TABLE")
notifyIsProduction := os.Getenv("GOVUK_NOTIFY_IS_PRODUCTION") == "1"
appPublicURL := os.Getenv("APP_PUBLIC_URL")
awsBaseURL := os.Getenv("AWS_BASE_URL")
notifyBaseURL := os.Getenv("GOVUK_NOTIFY_BASE_URL")
evidenceBucketName := os.Getenv("UPLOADS_S3_BUCKET_NAME")

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
Expand All @@ -61,6 +91,8 @@ func Handler(ctx context.Context, event events.CloudWatchEvent) error {
})
}

s3Client := s3.NewClient(cfg, evidenceBucketName)

dynamoClient, err := dynamo.NewClient(cfg, tableName)
if err != nil {
return fmt.Errorf("failed to create dynamodb client: %w", err)
Expand All @@ -86,37 +118,46 @@ func Handler(ctx context.Context, event events.CloudWatchEvent) error {
shareCodeSender := page.NewShareCodeSender(app.NewShareCodeStore(dynamoClient), notifyClient, appPublicURL, random.String)
now := time.Now

switch event.DetailType {
case "evidence-received":
return handleEvidenceReceived(ctx, dynamoClient, event)
case "fee-approved":
return handleFeeApproved(ctx, dynamoClient, event, shareCodeSender, appData, now)
case "more-evidence-required":
return handleMoreEvidenceRequired(ctx, dynamoClient, event, now)
case "fee-denied":
return handleFeeDenied(ctx, dynamoClient, event, now)
default:
return fmt.Errorf("unknown event received: %s", event.DetailType)
if event.isS3Event() {
return handleObjectTagsAdded(ctx, dynamoClient, event.S3Event, now, s3Client)
}

if event.isCloudWatchEvent() {
switch event.DetailType {
case evidenceReceivedEventName:
return handleEvidenceReceived(ctx, dynamoClient, event.CloudWatchEvent)
case feeApprovedEventName:
return handleFeeApproved(ctx, dynamoClient, event.CloudWatchEvent, shareCodeSender, appData, now)
case moreEvidenceRequiredEventName:
return handleMoreEvidenceRequired(ctx, dynamoClient, event.CloudWatchEvent, now)
case feeDeniedEventName:
return handleFeeDenied(ctx, dynamoClient, event.CloudWatchEvent, now)
default:
return fmt.Errorf("unknown event received: %s", event.DetailType)
}
}

eJson, _ := json.Marshal(event)
return fmt.Errorf("unknown event type received: %s", string(eJson))
}

func handleEvidenceReceived(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'evidence-received' detail: %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", evidenceReceivedEventName, err)
}

var key dynamo.Key
if err := client.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'evidence-received': %w", err)
return fmt.Errorf("failed to resolve uid for '%s': %w", evidenceReceivedEventName, err)
}

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'evidence-received'")
return fmt.Errorf("PK missing from LPA in response to '%s'", evidenceReceivedEventName)
}

if err := client.Put(ctx, map[string]string{"PK": key.PK, "SK": "#EVIDENCE_RECEIVED"}); err != nil {
return fmt.Errorf("failed to persist evidence received for 'evidence-received': %w", err)
return fmt.Errorf("failed to persist evidence received for '%s': %w", evidenceReceivedEventName, err)
}

return nil
Expand All @@ -125,28 +166,23 @@ func handleEvidenceReceived(ctx context.Context, client dynamodbClient, event ev
func handleFeeApproved(ctx context.Context, dynamoClient dynamodbClient, event events.CloudWatchEvent, shareCodeSender shareCodeSender, appData page.AppData, now func() time.Time) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'fee-approved' detail: %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", feeApprovedEventName, err)
}

var key dynamo.Key
if err := dynamoClient.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'fee-approved': %w", err)
}

var lpa page.Lpa
if err := dynamoClient.One(ctx, key.PK, key.SK, &lpa); err != nil {
return fmt.Errorf("failed to get LPA for 'fee-approved': %w", err)
lpa, err := getLpaByUID(ctx, dynamoClient, v.UID, feeApprovedEventName)
if err != nil {
return err
}

lpa.Tasks.PayForLpa = actor.PaymentTaskCompleted
lpa.UpdatedAt = now()

if err := dynamoClient.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for 'fee-approved': %w", err)
return fmt.Errorf("failed to update LPA task status for '%s': %w", feeApprovedEventName, err)
}

if err := shareCodeSender.SendCertificateProvider(ctx, notify.CertificateProviderInviteEmail, appData, false, &lpa); err != nil {
return fmt.Errorf("failed to send share code to certificate provider for 'fee-approved': %w", err)
return fmt.Errorf("failed to send share code to certificate provider for '%s': %w", feeApprovedEventName, err)
}

return nil
Expand All @@ -155,28 +191,19 @@ func handleFeeApproved(ctx context.Context, dynamoClient dynamodbClient, event e
func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'more-evidence-required' detail: %w", err)
}

var key dynamo.Key
if err := client.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'more-evidence-required': %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", moreEvidenceRequiredEventName, err)
}

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'more-evidence-required'")
}

var lpa page.Lpa
if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil {
return fmt.Errorf("failed to get LPA for 'more-evidence-required': %w", err)
lpa, err := getLpaByUID(ctx, client, v.UID, moreEvidenceRequiredEventName)
if err != nil {
return err
}

lpa.Tasks.PayForLpa = actor.PaymentTaskMoreEvidenceRequired
lpa.UpdatedAt = now()

if err := client.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for 'more-evidence-required': %w", err)
return fmt.Errorf("failed to update LPA task status for '%s': %w", moreEvidenceRequiredEventName, err)
}

return nil
Expand All @@ -185,33 +212,93 @@ func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, even
func handleFeeDenied(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error {
var v uidEvent
if err := json.Unmarshal(event.Detail, &v); err != nil {
return fmt.Errorf("failed to unmarshal 'fee-denied' detail: %w", err)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", feeDeniedEventName, err)
}

var key dynamo.Key
if err := client.OneByUID(ctx, v.UID, &key); err != nil {
return fmt.Errorf("failed to resolve uid for 'fee-denied': %w", err)
lpa, err := getLpaByUID(ctx, client, v.UID, feeDeniedEventName)
if err != nil {
return err
}

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'fee-denied'")
lpa.Tasks.PayForLpa = actor.PaymentTaskDenied
lpa.UpdatedAt = now()

if err := client.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for '%s': %w", feeDeniedEventName, err)
}

var lpa page.Lpa
if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil {
return fmt.Errorf("failed to get LPA for 'fee-denied': %w", err)
return nil
}

func handleObjectTagsAdded(ctx context.Context, client dynamodbClient, event events.S3Event, now func() time.Time, s3Client s3Client) error {
objectKey := event.Records[0].S3.Object.Key
if objectKey == "" {
return fmt.Errorf("object key missing in event in '%s'", objectTagsAddedEventName)
}

lpa.Tasks.PayForLpa = actor.PaymentTaskDenied
tags, err := s3Client.GetObjectTags(ctx, objectKey)
if err != nil {
return fmt.Errorf("failed to get tags for object in '%s': %w", objectTagsAddedEventName, err)
}

hasScannedTag := false
hasVirus := false

for _, tag := range tags {
if *tag.Key == "virus-scan-status" {
hasScannedTag = true
hasVirus = *tag.Value == virusFound
break
}
}

if !hasScannedTag {
return nil
}

uid := strings.Split(objectKey, "/")

lpa, err := getLpaByUID(ctx, client, uid[0], objectTagsAddedEventName)
if err != nil {
return err
}

document := lpa.Evidence.Get(objectKey)
if document.Key == "" {
return fmt.Errorf("LPA did not contain a document with key %s for '%s'", objectKey, objectTagsAddedEventName)
}

document.Scanned = now()
document.VirusDetected = hasVirus

lpa.Evidence.Put(document)
lpa.UpdatedAt = now()

if err := client.Put(ctx, lpa); err != nil {
return fmt.Errorf("failed to update LPA task status for 'fee-denied': %w", err)
return fmt.Errorf("failed to update LPA for '%s': %w", objectTagsAddedEventName, err)
}

return nil
}

func getLpaByUID(ctx context.Context, client dynamodbClient, uid, eventName string) (page.Lpa, error) {
var key dynamo.Key
if err := client.OneByUID(ctx, uid, &key); err != nil {
return page.Lpa{}, fmt.Errorf("failed to resolve uid for '%s': %w", eventName, err)
}

if key.PK == "" {
return page.Lpa{}, fmt.Errorf("PK missing from LPA in response to '%s'", eventName)
}

var lpa page.Lpa
if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil {
return page.Lpa{}, fmt.Errorf("failed to get LPA for '%s': %w", eventName, err)
}

return lpa, nil
}

func main() {
lambda.Start(Handler)
}
Loading

0 comments on commit 2bd35e6

Please sign in to comment.