Documentation
¶
Overview ¶
Package stale finds accounts nobody has signed in to for a long time, mails their owners, and, if the app asks for it, deletes the accounts that never answered. Stale accounts are an attack surface (a forgotten password reused elsewhere, an address that now belongs to someone else) and, in many jurisdictions, personal data that should not be kept for nothing.
Nothing runs by itself. The app calls Plugin.Run, or auth.Maintain, or "auth maintain" from the command line, from its own scheduler; the plugin has no goroutine and no timer. Each run is idempotent: it warns an account once, deletes it once, and a second run straight after finds nothing to do.
The steps, for an account inactive for InactiveAfter:
- Warn. A message (PurposeStaleAccount, English default wording, the delivering plugin's templates per locale win) goes to the user's best recovery channel, and the account is marked warned. The message says by when the account will be deleted, if deletion is on.
- Wait. Any sign-in clears the mark, so the account is judged afresh the next time it goes quiet. A live session still in use counts as activity too: a user kept signed in by a long session is not inactive.
- Delete (only under DeleteAfterWarning, off by default). Once the grace period after the warning has passed with no sign-in, a final message (PurposeAccountDeleted) is queued and the account is deleted, in one transaction: neither happens without the other.
What counts as the last activity is the newest of: the last sign-in the plugin recorded (it records one on every session created from the day it was installed), the account's creation, and Since. Accounts that predate the plugin have no recorded sign-in, so install it with Since set to the install time, or the first run will treat every old account as long inactive.
Index ¶
- Constants
- Variables
- type AccountDeletedData
- type ExemptFunc
- type Option
- type Plugin
- func (p *Plugin) Columns() []auth.ColumnExtension
- func (p *Plugin) DataHooks() auth.DataHooks
- func (p *Plugin) DecodeMessageData(purpose auth.Purpose, raw json.RawMessage) (any, error)
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) Maintain(ctx context.Context, o auth.MaintainOptions) (map[string]int64, error)
- func (p *Plugin) Name() string
- func (p *Plugin) OptionErr() error
- func (p *Plugin) Run(ctx context.Context, o RunOptions) (Result, error)
- func (p *Plugin) Validate(c *auth.Config) error
- type Result
- type RunOptions
- type StaleAccountData
Constants ¶
const ( // ColActiveAt is the time of the user's last recorded sign-in; NULL for a // user who has not signed in since the plugin was installed. //ds:def id=stale-colactiveat-3wq7nk5v owner=@khanakia stability=stable ColActiveAt = "stale_active_at" // ColWarnedAt is when the user was sent the stale-account warning; NULL // when they have not been, or have signed in since. ColWarnedAt = "stale_warned_at" )
Columns this plugin adds to auth_user.
const ( // PurposeStaleAccount warns the owner that the account has been inactive. PurposeStaleAccount auth.Purpose = "stale_account" // PurposeAccountDeleted tells the owner the account was deleted for // inactivity, after the warning went unanswered. PurposeAccountDeleted auth.Purpose = "account_deleted" )
Message purposes this plugin sends. Their Data are StaleAccountData and AccountDeletedData.
const ( // CounterWarned counts accounts sent the warning. CounterWarned = "warned" // CounterDeleted counts accounts deleted after the grace period. CounterDeleted = "deleted" // CounterCleared counts warned accounts returned to normal because a live // session was in use. CounterCleared = "cleared" // CounterActive counts stale-looking accounts skipped for a live session. CounterActive = "active" // CounterExempt counts accounts the ExemptFunc protected. CounterExempt = "exempt" // CounterNoChannel counts accounts with no recovery channel to warn. CounterNoChannel = "no_channel" // CounterFailed counts accounts that could not be processed. CounterFailed = "failed" )
Counter names of Result and of auth.Maintain (prefixed there with "stale.").
const ( // DefaultInactiveAfter is one year, a common retention line for a dormant // account. //ds:def id=stale-defaultinactiveafter-6mk2xq8n owner=@khanakia stability=stable DefaultInactiveAfter = 365 * 24 * time.Hour // MinInactiveAfter is the shortest accepted: a week. Anything shorter // warns people who simply went on holiday. MinInactiveAfter = 7 * 24 * time.Hour // MinDeletionGrace is the shortest time between warning and deletion: a // week, enough for a message to be read and acted on. MinDeletionGrace = 7 * 24 * time.Hour // DefaultBatchSize is how many accounts one run warns and how many it // deletes at most. It exists so a first run on an old install cannot mail // the whole user table at once; the next run carries on. //ds:def id=stale-defaultbatchsize-9vq3wn4k owner=@khanakia stability=stable DefaultBatchSize = 200 // MaxBatchSize caps BatchSize. MaxBatchSize = 10000 )
Defaults and bounds.
const ( // OutcomeWarned: the account was sent the inactivity warning. OutcomeWarned auth.RateOutcome = "warned" // OutcomeDeleted: the account was deleted after the grace period. OutcomeDeleted auth.RateOutcome = "deleted" )
Outcomes of the SecurityStaleAccount event, in SecurityEvent.Outcome: what the run did to the account. They extend the rate outcomes, as the event documents.
const PluginName = "stale"
PluginName is the plugin's permanent identity: the owner of its columns.
Variables ¶
var ( ActiveAt = sqlb.Col[*time.Time](auth.TableUser, ColActiveAt) WarnedAt = sqlb.Col[*time.Time](auth.TableUser, ColWarnedAt) )
Typed columns on auth_user, for an app that reports on dormant accounts in its own queries.
Functions ¶
This section is empty.
Types ¶
type AccountDeletedData ¶
type AccountDeletedData struct {
// WarnedAt is when the warning was sent.
WarnedAt time.Time `json:"warnedAt"`
// DeletedAt is when the account was deleted.
DeletedAt time.Time `json:"deletedAt"`
}
AccountDeletedData is Message.Data of PurposeAccountDeleted.
type ExemptFunc ¶
ExemptFunc says an account must never be warned or deleted: an admin, an owner of an organization, a billing contact. An error counts the account as failed for this run, and nothing is done to it.
type Option ¶
type Option func(*Plugin)
Option configures the plugin.
func BatchSize ¶
BatchSize caps how many accounts one run warns and how many it deletes (default DefaultBatchSize, at most MaxBatchSize).
func DeleteAfterWarning ¶
DeleteAfterWarning turns deletion on: an account still unanswered grace after its warning is deleted, with a final message. Off by default. grace is at least MinDeletionGrace. It needs auth.WithDeliveryOutbox, so the final message is stored in the same transaction as the deletion and sent (and retried) afterwards: without the outbox a crash could delete an account whose owner was never told. auth.New refuses the option without it.
func Exempt ¶
func Exempt(fn ExemptFunc) Option
Exempt protects accounts from being warned or deleted.
func InactiveAfter ¶
InactiveAfter sets how long an account may go without activity before its owner is warned (default DefaultInactiveAfter, at least MinInactiveAfter).
func Since ¶
Since sets the earliest time an account's inactivity counts from. Install the plugin with Since set to now: accounts that existed before it have no recorded sign-in, and without Since the first run would call every one of them inactive since it was created. Zero (the default) sets no floor.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the stale-account monitor. Build it with New.
func (*Plugin) Columns ¶
func (p *Plugin) Columns() []auth.ColumnExtension
Columns adds the last-activity and warned timestamps to auth_user.
func (*Plugin) DataHooks ¶
DataHooks record the activity: every session created for the user's own sign-in stamps ColActiveAt and clears a warning. An impersonation is an admin's activity, not the user's.
func (*Plugin) DecodeMessageData ¶
DecodeMessageData restores the payloads the delivery outbox stored as JSON. Any other purpose is not this plugin's: (nil, nil). The any result is the boundary type of Message.Data and of auth.MessageDataDecoder, which carries a different type per purpose; here it is always StaleAccountData or AccountDeletedData.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) Run ¶
Run does one pass: deletes the accounts whose grace period is over (when deletion is on), then warns the newly inactive ones, each at most BatchSize. It is idempotent, safe to run from several instances at once for the counters' purposes (a warning is marked in the transaction that queues it, so two racing runs may rarely both mail one account; that costs a duplicate message, never a missed one), and never aborts for one account: a failure is logged, counted in Result.Failed and retried by the next run. An error comes only from reading the candidates.
type Result ¶
type Result struct {
// Warned: accounts sent the warning.
Warned int64
// Deleted: accounts deleted after the grace period.
Deleted int64
// Cleared: warned accounts found active after all (a live session still
// in use) and returned to normal.
Cleared int64
// Active: stale-looking accounts skipped because a live session is in use.
Active int64
// Exempt: accounts the ExemptFunc protected.
Exempt int64
// NoChannel: accounts with no verified recovery channel to warn, which
// are therefore never deleted either.
NoChannel int64
// Failed: accounts that could not be processed (logged); a later run
// retries them.
Failed int64
}
Result counts what one run did (or, under DryRun, would do).
type RunOptions ¶
type RunOptions struct {
// DryRun counts without sending, marking or deleting. It cannot know who
// has no verified recovery channel, so Result.NoChannel stays 0 and those
// accounts are counted as warned.
DryRun bool
}
RunOptions are what one run is asked to do.
type StaleAccountData ¶
type StaleAccountData struct {
// LastActive is the time of the user's last activity as the plugin
// judged it.
LastActive time.Time `json:"lastActive"`
// InactiveDays is whole days since LastActive.
InactiveDays int `json:"inactiveDays"`
// DeleteAt is when the account will be deleted unless the user signs in;
// nil when deletion is off.
DeleteAt *time.Time `json:"deleteAt,omitempty"`
}
StaleAccountData is Message.Data of PurposeStaleAccount, for an app template to read with a type assertion.