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

Commit

Permalink
Add initial scaffolding for abuse prevention (#548)
Browse files Browse the repository at this point in the history
This adds the new realm fields and UI elements. The form is wired up to
save and persist them in the database, but there's no enforcement or
historical calculations yet.
  • Loading branch information
sethvargo authored Sep 15, 2020
1 parent a353ff4 commit bcf6d19
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 1 deletion.
91 changes: 90 additions & 1 deletion cmd/server/assets/realm.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<html lang="en">
<head>
{{template "head" .}}
{{template "floatingform" .}}
</head>

<body class="bg-light">
Expand Down Expand Up @@ -46,7 +47,7 @@ <h1>Realm settings</h1>
{{end}}


<form method="POST" action="/realm/settings/save">
<form method="POST" class="floating-form" action="/realm/settings/save">
{{ .csrfField }}

<div class="card mb-3 shadow-sm">
Expand Down Expand Up @@ -347,7 +348,66 @@ <h1>Realm settings</h1>
</small>
</div>
</div>
</div>
</div>

<div class="card mb-3 shadow-sm">
<div class="card-header">
Abuse prevention
</div>
<div class="card-body">
<div class="alert alert-warning">
<span class="oi oi-beaker"></span> This feature is still under
active development.
</div>

<p>
Abuse prevention uses the historical record of your realm's past
daily code issuances to build a predictive model of future use,
rejecting requests that fall outside of the predicted model.
</p>

<div class="form-group form-check">
<input class="form-check-input" type="checkbox" name="abuse_prevention_enabled" id="abuse_prevention_enabled" value="1" {{if $realm.AbusePreventionEnabled}} checked{{end}}>
<label class="form-check-label" for="abuse_prevention_enabled">
Enable abuse prevention
</label>
</div>

<div id="abuse_prevention_configuration" class="{{if not $realm.AbusePreventionEnabled}}d-none{{end}}">
<div class="form-label-group">
<input type="text" id="abuse_prevention_limit" name="abuse_prevention_limit" class="form-control" placeholder="Computed limit" value="{{$realm.AbusePreventionLimit}}" readonly />
<label for="abuse_prevention_limit">Computed limit</label>
<small class="form-text text-muted">
This value is computed by the historical daily model and applies
for the next 24h block of rolling UTC time.
</small>
</div>

<div class="form-label-group">
<input type="text" id="abuse_prevention_limit_factor" name="abuse_prevention_limit_factor" class="form-control" placeholder="Limit factor" value="{{printf "%.3f" $realm.AbusePreventionLimitFactor}}" />
<label for="abuse_prevention_limit_factor">Limit factor</label>
<small class="form-text text-muted">
This value is factored against the predicted daily model to
determine the total number of codes that {{$realm.Name}} can issue
in a day. For example, to enable 25% more codes to be issued than
predicted by the model model, set this value to <code>1.25</code>.
</small>
<small class="form-text text-danger font-weight-bold">
Setting this value too low will prevent case workers from issuing
codes for legitimate uses!
</small>
</div>

<div class="form-label-group">
<input type="text" id="abuse_prevention_effective_limit" class="form-control" placeholder="Effective limit" value="{{$realm.AbusePreventionEffectiveLimit}}" readonly />
<label for="abuse_prevention_effective_limit">Effective limit</label>
<small class="form-text text-muted">
This is the effective daily limit for {{$realm.Name}} after
applying your limit factor.
</small>
</div>
</div>
</div>
</div>

Expand Down Expand Up @@ -416,6 +476,35 @@ <h1>Realm settings</h1>
</main>

{{template "scripts" .}}

<script type="text/javascript">
$(function() {
let $abusePreventionEnabled = $('#abuse_prevention_enabled');
let $abusePreventionConfiguration = $('#abuse_prevention_configuration');
let $abusePreventionLimit = $('#abuse_prevention_limit');
let $abusePreventionLimitFactor = $('#abuse_prevention_limit_factor');
let $abusePreventionEffectiveLimit = $('#abuse_prevention_effective_limit');

$abusePreventionEnabled.change(function(e) {
if (this.checked) {
$abusePreventionConfiguration.removeClass('d-none');
} else {
$abusePreventionConfiguration.addClass('d-none');
}
});

$abusePreventionLimitFactor.keyup(function(e){
let $this = $(e.currentTarget);
let current = $this.val();

if (current && current.length) {
let effective = parseFloat(current) * parseFloat($abusePreventionLimit.val());
effective = Math.ceil(effective);
$abusePreventionEffectiveLimit.val(effective);
}
});
});
</script>
</body>
</html>
{{end}}
5 changes: 5 additions & 0 deletions pkg/controller/realmadmin/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func (c *Controller) HandleSave() http.Handler {
TwilioAccountSid string `form:"twilio_account_sid"`
TwilioAuthToken string `form:"twilio_auth_token"`
TwilioFromNumber string `form:"twilio_from_number"`

AbusePreventionEnabled bool `form:"abuse_prevention_enabled"`
AbusePreventionLimitFactor float32 `form:"abuse_prevention_limit_factor"`
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -101,6 +104,8 @@ func (c *Controller) HandleSave() http.Handler {
realm.LongCodeDuration.Duration = time.Hour * time.Duration(form.LongCodeHours)
realm.SMSTextTemplate = form.SMSTextTemplate
realm.MFAMode = database.MFAMode(form.MFAMode)
realm.AbusePreventionEnabled = form.AbusePreventionEnabled
realm.AbusePreventionLimitFactor = form.AbusePreventionLimitFactor
if err := c.db.SaveRealm(realm); err != nil {
flash.Error("Failed to update realm: %v", err)
c.renderShow(ctx, w, r, realm, nil)
Expand Down
33 changes: 33 additions & 0 deletions pkg/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,39 @@ func (db *Database) getMigrations(ctx context.Context) *gormigrate.Gormigrate {
return nil
},
},
{
ID: "00041-AddRealmAbuseProtection",
Migrate: func(tx *gorm.DB) error {
sqls := []string{
`ALTER TABLE realms ADD COLUMN IF NOT EXISTS abuse_prevention_enabled bool NOT NULL DEFAULT false`,
`ALTER TABLE realms ADD COLUMN IF NOT EXISTS abuse_prevention_limit integer NOT NULL DEFAULT 100`,
`ALTER TABLE realms ADD COLUMN IF NOT EXISTS abuse_prevention_limit_factor numeric(8, 5) NOT NULL DEFAULT 1.0`,
}

for _, sql := range sqls {
if err := tx.Exec(sql).Error; err != nil {
return err
}
}

return nil
},
Rollback: func(tx *gorm.DB) error {
sqls := []string{
`ALTER TABLE realms DROP COLUMN IF EXISTS abuse_prevention_enabled`,
`ALTER TABLE realms DROP COLUMN IF EXISTS abuse_prevention_limit`,
`ALTER TABLE realms DROP COLUMN IF EXISTS abuse_prevention_limit_factor`,
}

for _, sql := range sqls {
if err := tx.Exec(sql).Error; err != nil {
return err
}
}

return nil
},
},
})
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/database/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"

Expand Down Expand Up @@ -105,6 +106,22 @@ type Realm struct {
// EN Express
EnableENExpress bool `gorm:"type:boolean; default: false"`

// AbusePreventionEnabled determines if abuse protection is enabled.
AbusePreventionEnabled bool `gorm:"type:boolean; not null; default:false"`

// AbusePreventionLimit is the configured daily limit for the realm. This value is populated
// by the nightly aggregation job and is based on a statistical model from
// historical code issuance data.
AbusePreventionLimit uint `gorm:"type:integer; not null; default:100"`

// AbusePreventionLimitFactor is the factor against the predicted model for the day which
// determines the total number of codes that can be issued for the realm on
// the day. For example, if the predicted value was 50 and this value was 1.5,
// the realm could generate 75 codes today before triggering abuse prevention.
// Similarly, if this value was 0.5, the realm could only generate 25 codes
// before triggering abuse protections.
AbusePreventionLimitFactor float32 `gorm:"type:numeric(6, 3); not null; default:1.0"`

// These are here for gorm to setup the association. You should NOT call them
// directly, ever. Use the ListUsers function instead. The have to be public
// for reflection.
Expand Down Expand Up @@ -322,6 +339,15 @@ func (r *Realm) SMSProvider(db *Database) (sms.Provider, error) {
return provider, nil
}

// AbusePreventionEffectiveLimit returns the effective limit, multiplying the limit by the
// limit factor and rounding up.
func (r *Realm) AbusePreventionEffectiveLimit() uint {
// Only maintain 3 digits of precision, since that's all we do in the
// database.
factor := math.Floor(float64(r.AbusePreventionLimitFactor)*100) / 100
return uint(math.Ceil(float64(r.AbusePreventionLimit) * float64(factor)))
}

// GetCurrentSigningKey returns the currently active signing key, the one marked
// active in the database. If there is more than one active, the most recently
// created one wins. Should not occur due to transactional update.
Expand Down

0 comments on commit bcf6d19

Please sign in to comment.