Skip to content

Time-limited access

Banks and regulated businesses often need access that’s temporary: an approver covering leave, a contractor until the end of the month, an auditor for one week.

err := w.Assign(ctx, "tolu", "loan_officer", lagos,
wardn.From(time.Date(2026, 11, 9, 9, 0, 0, 0, lagosTZ)),
wardn.Until(time.Date(2026, 11, 13, 17, 0, 0, 0, lagosTZ)))

When your code computes the window, use wardn’s clock, so tests that move it move yours too:

err := w.Assign(ctx, doctor, "doctor", cardiology, wardn.Until(w.Now().Add(72*time.Hour)))
  • From alone: access starts later and never ends. Until alone: access starts now and ends later.
  • The window includes its start and excludes its end: at exactly 17:00 on the 13th, access is gone.
  • Assigning again replaces the window, for example to extend it, or to make it permanent by passing neither option.
  • Windows must end after they start, may not already have ended, and must fall within years 1 to 9999.

The window is checked on every request against the current time, so access stops at exactly the right moment, even if nothing ever deletes the assignment. This also holds with caching: wardn caches the assignment, never the answer.

Regulators expect a record of when access ended. The expiry sweeper reports each expired assignment through your audit hook as an assignment.expired event:

// Run in its own goroutine; safe on every instance.
go w.RunExpiry(ctx, time.Minute, func(err error) { log.Error("expiry sweep", "err", err) })

Or call w.SweepExpired(ctx) from your own scheduler. Pass wardn.RemoveExpired() to also delete expired assignments once they’re reported.

If your audit hook fails, the sweep stops, and those expiries are reported again next time. An expiry can be reported more than once (after a failure, or when several instances sweep at the same moment), so deduplicate on user, role, scope and expires_at if your trail needs exactly one record. An assignment that was granted again while a sweep was running is never removed.

Use a fake clock to move time forward in tests:

clock := wardntest.NewClock(time.Date(2026, 11, 9, 9, 0, 0, 0, time.UTC))
w := wardntest.New(t, "wardn.yaml", wardntest.WithClock(clock))
// ... assign with Until(...)
clock.Advance(48 * time.Hour)
// ... check that access has ended

Each server checks time windows against its own clock. Nothing it shares with other servers (the database or the cache) carries a time or a decision. So a server whose clock is off is wrong by exactly its skew and no longer. If a clock is 2 seconds slow, access starts 2 seconds late and ends 2 seconds late on that server, and on time everywhere else.

Questions about this page?

Ask below, point out something wrong or unclear, or suggest an improvement. Reading needs no account; to post, sign in with GitHub.