Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

adjust symptom onset handling #1131

Merged
merged 5 commits into from
Nov 10, 2020
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
14 changes: 7 additions & 7 deletions internal/generate/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ type Config struct {
MaxSameStartIntervalKeys uint `env:"MAX_SAME_START_INTERVAL_KEYS, default=2"`
SimulateSameDayRelease bool `env:"SIMULATE_SAME_DAY_RELEASE, default=false"`
MaxIntervalAge time.Duration `env:"MAX_INTERVAL_AGE_ON_PUBLISH, default=360h"`
MaxMagnitudeSymptomOnsetDays uint `env:"MAX_SYMPTOM_ONSET_DAYS, default=21"`
MaxMagnitudeSymptomOnsetDays uint `env:"MAX_SYMPTOM_ONSET_DAYS, default=14"`
MaxSypmtomOnsetReportDays uint `env:"MAX_VALID_SYMPOTOM_ONSET_REPORT_DAYS, default=28"`
CreatedAtTruncateWindow time.Duration `env:"TRUNCATE_WINDOW, default=1h"`
DefaultRegion string `env:"DEFAULT_REGION, default=US"`
ChanceOfKeyRevision int `env:"CHANCE_OF_KEY_REVISION, default=30"` // 0-100 are valid values.
ChanceOfTraveler int `env:"CHANCE_OF_TRAVELER, default=20"` // 0-100 are valid values
KeyRevisionDelay time.Duration `env:"KEY_REVISION_DELAY, default=2h"` // key revision will be forward dates this amount.
UseDefaultSymptomOnset bool `env:"USE_DEFAULT_SYMPTOM_ONSET_DAYS, default=true"`
SymptomOnsetDays uint `env:"DEFAULT_SYMPTOM_ONSET_DAYS, default=10"`
SymptomOnsetDaysAgo uint `env:"DEFAULT_SYMPTOM_ONSET_DAYS_AGO, default=4"`
}

func (c *Config) MaxExposureKeys() uint {
Expand All @@ -75,12 +75,12 @@ func (c *Config) MaxSymptomOnsetDays() uint {
return c.MaxMagnitudeSymptomOnsetDays
}

func (c *Config) UseDefaultSymptomOnsetDays() bool {
return c.UseDefaultSymptomOnset
func (c *Config) MaxValidSymptomOnsetReportDays() uint {
return c.MaxSypmtomOnsetReportDays
}

func (c *Config) DefaultSymptomOnsetDays() int32 {
return int32(c.SymptomOnsetDays)
func (c *Config) DefaultSymptomOnsetDaysAgo() uint {
return c.SymptomOnsetDaysAgo
}

func (c *Config) DebugReleaseSameDayKeys() bool {
Expand Down
25 changes: 13 additions & 12 deletions internal/integration/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,18 +207,19 @@ func NewTestServer(tb testing.TB) (*serverenv.ServerEnv, *Client) {
}
mux.Handle("/key-rotation/", http.StripPrefix("/key-rotation", rotationServer.Routes(ctx)))

// Publish
publishConfig := &publish.Config{
RevisionToken: revConfig,
MaxKeysOnPublish: 15,
MaxSameStartIntervalKeys: 2,
MaxIntervalAge: 360 * time.Hour,
CreatedAtTruncateWindow: 1 * time.Second,
ReleaseSameDayKeys: true,
RevisionKeyCacheDuration: time.Second,
}

publishHandler, err := publish.NewHandler(ctx, publishConfig, env)
// Parse the config to load default values.
publishConfig := publish.Config{}
envconfig.ProcessWith(ctx, &publishConfig, envconfig.OsLookuper())
// Make overrides.
publishConfig.RevisionToken = revConfig
publishConfig.MaxKeysOnPublish = 15
publishConfig.MaxSameStartIntervalKeys = 2
publishConfig.MaxIntervalAge = 360 * time.Hour
publishConfig.CreatedAtTruncateWindow = time.Second
publishConfig.ReleaseSameDayKeys = true
publishConfig.RevisionKeyCacheDuration = time.Second
mikehelmick marked this conversation as resolved.
Show resolved Hide resolved

publishHandler, err := publish.NewHandler(ctx, &publishConfig, env)
if err != nil {
tb.Fatal(err)
}
Expand Down
5 changes: 5 additions & 0 deletions internal/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,9 @@ func getExposures(db *database.DB, criteria publishdb.IterateExposuresCriteria)
// output).
func exportedKeysFrom(tb testing.TB, keys []verifyapi.ExposureKey) []*export.TemporaryExposureKey {
s := make([]*export.TemporaryExposureKey, len(keys))
sort.Slice(keys, func(i, j int) bool {
return keys[i].IntervalNumber >= keys[j].IntervalNumber
})
for i, key := range keys {
decoded, err := base64util.DecodeString(key.Key)
if err != nil {
Expand All @@ -428,6 +431,8 @@ func exportedKeysFrom(tb testing.TB, keys []verifyapi.ExposureKey) []*export.Tem
TransmissionRiskLevel: proto.Int32(int32(key.TransmissionRisk)),
RollingStartIntervalNumber: proto.Int32(key.IntervalNumber),
ReportType: export.TemporaryExposureKey_CONFIRMED_TEST.Enum(),
// Keys are generated 1 day ago and then -1 day for each additional.
DaysSinceOnsetOfSymptoms: proto.Int32(int32(3 - i)),
}
}

Expand Down
49 changes: 36 additions & 13 deletions internal/publish/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package publish

import (
"fmt"
"time"

"github.com/google/exposure-notifications-server/internal/authorizedapp"
Expand All @@ -27,6 +28,7 @@ import (
"github.com/google/exposure-notifications-server/pkg/keys"
"github.com/google/exposure-notifications-server/pkg/observability"
"github.com/google/exposure-notifications-server/pkg/secrets"
"github.com/hashicorp/go-multierror"
)

// Compile-time check to assert this config matches requirements.
Expand All @@ -51,15 +53,21 @@ type Config struct {
Port string `env:"PORT, default=8080"`
MaxKeysOnPublish uint `env:"MAX_KEYS_ON_PUBLISH, default=30"`
// Provides compatibility w/ 1.5 release.
MaxSameStartIntervalKeys uint `env:"MAX_SAME_START_INTERVAL_KEYS, default=3"`
MaxIntervalAge time.Duration `env:"MAX_INTERVAL_AGE_ON_PUBLISH, default=360h"`
MaxMagnitudeSymptomOnsetDays uint `env:"MAX_SYMPTOM_ONSET_DAYS, default=10"`
CreatedAtTruncateWindow time.Duration `env:"TRUNCATE_WINDOW, default=1h"`

// If set, TEKs that arrive without a days since symptom onset (i.e. no symptom onset date)
// will be set to the default symptom onset days.
UseDefaultSymptomOnset bool `env:"USE_DEFAULT_SYMPTOM_ONSET_DAYS, default=true"`
SymptomOnsetDays uint `env:"DEFAULT_SYMPTOM_ONSET_DAYS, default=10"`
MaxSameStartIntervalKeys uint `env:"MAX_SAME_START_INTERVAL_KEYS, default=3"`
MaxIntervalAge time.Duration `env:"MAX_INTERVAL_AGE_ON_PUBLISH, default=360h"`
CreatedAtTruncateWindow time.Duration `env:"TRUNCATE_WINDOW, default=1h"`

// Symptom onset settings.
// Maximum valid range. TEKs presneted with values outside this range, but still "reasonable" will not be saved.
MaxMagnitudeSymptomOnsetDays uint `env:"MAX_SYMPTOM_ONSET_DAYS, default=14"`
// MaxValidSypmtomOnsetReportDays indicates how many days would be considered
// a valid symptom onset report (-val..+val). Anything outside
// that range would be subject to the default symptom onset flags (see below).
MaxSypmtomOnsetReportDays uint `env:"MAX_VALID_SYMPOTOM_ONSET_REPORT_DAYS, default=28"`

// TEKs that arrive without a days since symptom onset (i.e. no symptom onset date),
// then the upload date minus DEFAULT_SYMPTOM_ONSET_DAYS_AGO is used.
SymptomOnsetDaysAgo uint `env:"DEFAULT_SYMPTOM_ONSET_DAYS_AGO, default=4"`

ResponsePaddingMinBytes int64 `env:"RESPONSE_PADDING_MIN_BYTES, default=1024"`
ResponsePaddingRange int64 `env:"RESPONSE_PADDING_RANGE, default=1024"`
Expand Down Expand Up @@ -91,6 +99,21 @@ type Config struct {
DebugLogBadCertificates bool `env:"DEBUG_LOG_BAD_CERTIFICATES"`
}

func (c *Config) Validate() error {
var result *multierror.Error

if c.MaxMagnitudeSymptomOnsetDays == 0 {
result = multierror.Append(result,
fmt.Errorf("env var `MAX_SYMPTOM_ONSET_DAYS` must be > 0, got: %v", c.MaxMagnitudeSymptomOnsetDays))
}
if c.MaxSypmtomOnsetReportDays == 0 {
result = multierror.Append(result,
fmt.Errorf("env var `MAX_VALID_SYMPOTOM_ONSET_REPORT_DAYS` must be > 0, got: %v", c.MaxSypmtomOnsetReportDays))
}

return result.ErrorOrNil()
}

func (c *Config) MaxExposureKeys() uint {
return c.MaxKeysOnPublish
}
Expand All @@ -111,12 +134,12 @@ func (c *Config) MaxSymptomOnsetDays() uint {
return c.MaxMagnitudeSymptomOnsetDays
}

func (c *Config) UseDefaultSymptomOnsetDays() bool {
return c.UseDefaultSymptomOnset
func (c *Config) MaxValidSymptomOnsetReportDays() uint {
return c.MaxSypmtomOnsetReportDays
}

func (c *Config) DefaultSymptomOnsetDays() int32 {
return int32(c.SymptomOnsetDays)
func (c *Config) DefaultSymptomOnsetDaysAgo() uint {
return c.SymptomOnsetDaysAgo
}

func (c *Config) DebugReleaseSameDayKeys() bool {
Expand Down
90 changes: 51 additions & 39 deletions internal/publish/model/exposure_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/google/exposure-notifications-server/internal/verification"
"github.com/google/exposure-notifications-server/pkg/base64util"
"github.com/google/exposure-notifications-server/pkg/logging"
"github.com/google/exposure-notifications-server/pkg/timeutils"
"github.com/hashicorp/go-multierror"

verifyapi "github.com/google/exposure-notifications-server/pkg/api/v1"
Expand Down Expand Up @@ -379,21 +380,21 @@ type TransformerConfig interface {
MaxIntervalStartAge() time.Duration
TruncateWindow() time.Duration
MaxSymptomOnsetDays() uint
UseDefaultSymptomOnsetDays() bool
DefaultSymptomOnsetDays() int32
MaxValidSymptomOnsetReportDays() uint
DefaultSymptomOnsetDaysAgo() uint
DebugReleaseSameDayKeys() bool
}

// Transformer represents a configured Publish -> Exposure[] transformer.
type Transformer struct {
maxExposureKeys int // Overall maximum number of keys.
maxSameDayKeys int // Number of keys that are allowed to have the same start interval.
maxIntervalStartAge time.Duration // How many intervals old does this server accept?
truncateWindow time.Duration
maxSymptomOnsetDays float64 // to avoid casting in comparisons
useDefaultSymptomOnsetDays bool
defaultSymptomOnsetDays int32
debugReleaseSameDay bool // If true, still valid keys are not embargoed.
maxExposureKeys int // Overall maximum number of keys.
maxSameDayKeys int // Number of keys that are allowed to have the same start interval.
maxIntervalStartAge time.Duration // How many intervals old does this server accept?
truncateWindow time.Duration
maxSymptomOnsetDays float64 // to avoid casting in comparisons
maxValidSymptomOnsetReportDays uint
defaultSymptomOnsetDaysAgo uint
debugReleaseSameDay bool // If true, still valid keys are not embargoed.
}

// NewTransformer creates a transformer for turning publish API requests into
Expand All @@ -407,14 +408,14 @@ func NewTransformer(config TransformerConfig) (*Transformer, error) {
return nil, fmt.Errorf("maxSameDayKeys must be >= 1, got %v", config.MaxSameDayKeys())
}
return &Transformer{
maxExposureKeys: int(config.MaxExposureKeys()),
maxSameDayKeys: int(config.MaxSameDayKeys()),
maxIntervalStartAge: config.MaxIntervalStartAge(),
truncateWindow: config.TruncateWindow(),
maxSymptomOnsetDays: float64(config.MaxSymptomOnsetDays()),
useDefaultSymptomOnsetDays: config.UseDefaultSymptomOnsetDays(),
defaultSymptomOnsetDays: config.DefaultSymptomOnsetDays(),
debugReleaseSameDay: config.DebugReleaseSameDayKeys(),
maxExposureKeys: int(config.MaxExposureKeys()),
maxSameDayKeys: int(config.MaxSameDayKeys()),
maxIntervalStartAge: config.MaxIntervalStartAge(),
truncateWindow: config.TruncateWindow(),
maxSymptomOnsetDays: float64(config.MaxSymptomOnsetDays()),
maxValidSymptomOnsetReportDays: config.MaxValidSymptomOnsetReportDays(),
defaultSymptomOnsetDaysAgo: config.DefaultSymptomOnsetDaysAgo(),
debugReleaseSameDay: config.DebugReleaseSameDayKeys(),
}, nil
}

Expand Down Expand Up @@ -559,11 +560,33 @@ func (t *Transformer) TransformPublish(ctx context.Context, inData *verifyapi.Pu
BatchWindow: t.truncateWindow,
}

onsetInterval := inData.SymptomOnsetInterval
// If the symtom onset interval provided on publish is too old to be relevant
// and one was provided in the verification certificate, take that one.
if onsetInterval < settings.MinStartInterval && claims != nil && claims.SymptomOnsetInterval > 0 {
onsetInterval = int32(claims.SymptomOnsetInterval)
// For validating key timing information, can't be newer than now.
currentInterval := IntervalNumber(batchTime)
// For validating the passed in symptom interval, relative to current time.
minSymptomInterval := IntervalNumber(
timeutils.UTCMidnight(timeutils.SubtractDays(batchTime, t.maxValidSymptomOnsetReportDays)))

// Base level, assume there is no symptom onset interval present.
onsetInterval := int32(0)
if pubInt := inData.SymptomOnsetInterval; pubInt < currentInterval && pubInt >= minSymptomInterval {
onsetInterval = pubInt
} else if claims != nil {
if vcInt := int32(claims.SymptomOnsetInterval); vcInt < currentInterval && vcInt >= minSymptomInterval {
// If the symtom onset interval provided on publish is too old to be relevant
// and one was provided in the verification certificate, take that one.
onsetInterval = int32(claims.SymptomOnsetInterval)
}
}
// If we reach this point, and onsetInterval is 0 OR if the onset interval
// is "unreasonable" then we default the onsetInterval to 4 (*configurable)
// days ago to approximate symptom onset.
//
// There are launched applications using this sever that rely on this
// behavior - that are passing invalid symotom onset interviews, those
// are screened about above when the onsetInterval is set.
if daysSince := math.Abs(float64(DaysFromSymptomOnset(onsetInterval, currentInterval))); onsetInterval == 0 || daysSince > float64(t.maxValidSymptomOnsetReportDays) {
logger.Debugw("defaulting days since symptom onset")
onsetInterval = IntervalNumber(timeutils.SubtractDays(batchTime, t.defaultSymptomOnsetDaysAgo))
}

// Regions are a multi-value property, uppercase them for storage.
Expand Down Expand Up @@ -597,29 +620,18 @@ func (t *Transformer) TransformPublish(ctx context.Context, inData *verifyapi.Pu
// Set days since onset, either from the API or from the verified claims (see above).
if onsetInterval > 0 {
daysSince := DaysFromSymptomOnset(onsetInterval, exposure.IntervalNumber)
// Check if the magnitude of this value is too large. If it is too large,
// we won't want to set a days since symptom onset value on the TEK
// itself, but we do want to warn the application developer that this
// value (not TEK) was dropped.
//
// There are launched applications using this sever that rely on this
// behavior.
//
// Note that previously this returned an error, but this broke the iOS
// implementation since it is unable to handle partial success. As such,
// it was converted to a warning that's a separate field in the API
// response.
if abs := math.Abs(float64(daysSince)); abs > t.maxSymptomOnsetDays {
logger.Debugw("setting days since symptom onset to null on key due to symptom onset magnitude too high", "daysSince", daysSince)
transformWarnings = append(transformWarnings, fmt.Sprintf("key %d symptom onset is too large, %v > %v - saving without days since symptom onset", i, abs, t.maxSymptomOnsetDays))
} else {
// The value is within acceptable range, save it.
exposure.SetDaysSinceSymptomOnset(daysSince)
transformWarnings = append(transformWarnings, fmt.Sprintf("key %d symptom onset is too large, %v > %v - saving without this key", i, abs, t.maxSymptomOnsetDays))
continue
}
}
// See if a default symptom onset days should be applied.
if exposure.DaysSinceSymptomOnset == nil && t.useDefaultSymptomOnsetDays {
exposure.SetDaysSinceSymptomOnset(t.defaultSymptomOnsetDays)

// The value is within acceptable range, save it.
exposure.SetDaysSinceSymptomOnset(daysSince)
}

exposure.Traveler = inData.Traveler
Expand Down
Loading