Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MLPAB-1472: Update virus scanning results on evidence in dynamodb #779

Merged
merged 32 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
65ad134
handle document-scanned events
acsauk Oct 17, 2023
4e9afd3
drop unused struct
acsauk Oct 17, 2023
8a457a1
Merge branch 'main' into MLPAB-1472
acsauk Oct 18, 2023
34291f8
make clear we are getting a document
acsauk Oct 18, 2023
fc82dde
add object tags added event commands
acsauk Oct 18, 2023
a4c2609
ignore all but app main.go in air (as we dont rebuild them)
acsauk Oct 18, 2023
5fe0827
handle objectTagsAdded events
acsauk Oct 18, 2023
8a72e06
Merge branch 'main' into MLPAB-1472
acsauk Oct 18, 2023
cc06e4e
cov bump
acsauk Oct 18, 2023
d128bb7
tidy
acsauk Oct 19, 2023
f350c67
Merge branch 'main' into MLPAB-1472
acsauk Oct 19, 2023
7621d50
Merge branch 'main' into MLPAB-1472
acsauk Oct 19, 2023
7974d9f
simplify tag checking, bring Evidence funcs in line with other collec…
acsauk Oct 20, 2023
58e91f4
Merge branch 'MLPAB-1472' of github.com:ministryofjustice/opg-moderni…
acsauk Oct 20, 2023
8facfd6
Merge branch 'main' into MLPAB-1472
acsauk Oct 20, 2023
14fdc92
Merge branch 'main' into MLPAB-1472
acsauk Oct 20, 2023
12a2a22
allow s3:ObjectTagging:Put events to be seen in EventBridge
acsauk Oct 20, 2023
f58d861
Merge branch 'MLPAB-1472' of github.com:ministryofjustice/opg-moderni…
acsauk Oct 20, 2023
7a25379
actually add event to to the real resource
acsauk Oct 20, 2023
a9d6c76
debug
acsauk Oct 20, 2023
b91790a
revert terraform changes
acsauk Oct 23, 2023
86134f3
Merge branch 'main' into MLPAB-1472
acsauk Oct 23, 2023
0360bca
Merge branch 'main' into MLPAB-1472
acsauk Oct 23, 2023
f41af23
see the event
acsauk Oct 23, 2023
90ad9bf
handle S3Events
acsauk Oct 23, 2023
f6a70d0
update tests
acsauk Oct 23, 2023
bdf265d
Merge branch 'main' into MLPAB-1472
acsauk Oct 23, 2023
c1eb0ee
set bucket name
acsauk Oct 23, 2023
4c73416
allow getting tags for objects rather than bucket
acsauk Oct 23, 2023
60cc1c9
use bucket name
acsauk Oct 23, 2023
49d8c45
Merge branch 'main' into MLPAB-1472
acsauk Oct 24, 2023
98bdfd8
Merge branch 'main' into MLPAB-1472
acsauk Oct 24, 2023
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
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
165 changes: 121 additions & 44 deletions cmd/event-received/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,59 @@ 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 = "Object Tags Added"
)

type uidEvent struct {
UID string `json:"uid"`
}

type objectTagsAddedEvent struct {
Object struct {
Key string `json:"key"`
} `json:"object"`
}

//go:generate mockery --testonly --inpackage --name dynamodbClient --structname mockDynamodbClient
type dynamodbClient interface {
One(ctx context.Context, pk, sk string, v interface{}) error
OneByUID(ctx context.Context, uid string, v interface{}) error
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
Expand All @@ -45,6 +67,7 @@ func Handler(ctx context.Context, event events.CloudWatchEvent) error {
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 +84,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 @@ -87,14 +112,16 @@ func Handler(ctx context.Context, event events.CloudWatchEvent) error {
now := time.Now

switch event.DetailType {
case "evidence-received":
case evidenceReceivedEventName:
return handleEvidenceReceived(ctx, dynamoClient, event)
case "fee-approved":
case feeApprovedEventName:
return handleFeeApproved(ctx, dynamoClient, event, shareCodeSender, appData, now)
case "more-evidence-required":
case moreEvidenceRequiredEventName:
return handleMoreEvidenceRequired(ctx, dynamoClient, event, now)
case "fee-denied":
case feeDeniedEventName:
return handleFeeDenied(ctx, dynamoClient, event, now)
case objectTagsAddedEventName:
return handleObjectTagsAdded(ctx, dynamoClient, event, now, s3Client)
default:
return fmt.Errorf("unknown event received: %s", event.DetailType)
}
Expand All @@ -103,20 +130,20 @@ func Handler(ctx context.Context, event events.CloudWatchEvent) error {
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 +152,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,63 +177,118 @@ 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)
return fmt.Errorf("failed to unmarshal '%s' detail: %w", moreEvidenceRequiredEventName, 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)
lpa, err := getLpaByUID(ctx, client, v.UID, moreEvidenceRequiredEventName)
if err != nil {
return err
}

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'more-evidence-required'")
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 '%s': %w", moreEvidenceRequiredEventName, 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 'more-evidence-required': %w", err)
return nil
}

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 '%s' detail: %w", feeDeniedEventName, err)
}

lpa.Tasks.PayForLpa = actor.PaymentTaskMoreEvidenceRequired
lpa, err := getLpaByUID(ctx, client, v.UID, feeDeniedEventName)
if err != nil {
return err
}

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 'more-evidence-required': %w", err)
return fmt.Errorf("failed to update LPA task status for '%s': %w", feeDeniedEventName, err)
}

return nil
}

func handleFeeDenied(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error {
var v uidEvent
func handleObjectTagsAdded(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time, s3Client s3Client) error {
var v objectTagsAddedEvent
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", objectTagsAddedEventName, 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)
objectKey := v.Object.Key

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

if key.PK == "" {
return errors.New("PK missing from LPA in response to 'fee-denied'")
hasScannedTag := false
var tagIndex int

for i, tag := range tags {
if *tag.Key == "virus-scan-status" {
hasScannedTag = true
tagIndex = i
acsauk marked this conversation as resolved.
Show resolved Hide resolved
}
}

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)
if !hasScannedTag {
return nil
}

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

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

document := lpa.Evidence.GetByDocumentKey(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 = *tags[tagIndex].Value == virusFound

if ok := lpa.Evidence.Update(document); !ok {
return fmt.Errorf("failed to update evidence on LPA for '%s'", objectTagsAddedEventName)
}

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 '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