diff --git a/hallpass.go b/hallpass.go index a1289af..8074d13 100644 --- a/hallpass.go +++ b/hallpass.go @@ -330,6 +330,9 @@ func (at *accessTypes) DurationOptions() (ret []durationDropdown) { cands = append(cands, d) } } + // why: never offer a duration above the absolute MaxTTL cap, even if a + // per-access-type Max is configured higher. + max = min(max, MaxTTL) slices.Sort(cands) cands = slices.Compact(cands) for _, d := range cands { @@ -435,6 +438,10 @@ func (tds timeDurationString) MarshalJSON() ([]byte, error) { const grantApp = "github.com/tailscale/hallpass" +// MaxTTL is the absolute maximum duration any hallpass access grant may be +// issued for, regardless of per-access-type Max configuration. +const MaxTTL = 12 * time.Hour + func (s *Server) lookup(r *http.Request) (ret lookupInfo, err error) { who, err := s.localClient.WhoIs(r.Context(), r.RemoteAddr) if err != nil { @@ -579,7 +586,7 @@ func (s *Server) authorize(ctx context.Context, li lookupInfo, req authorizeRequ if !ok { return fmt.Errorf("user %q not allowed to request access key %q", li.Who, req.AttrKey) } - maxDur := cmp.Or(time.Duration(at.Max), 24*time.Hour) + maxDur := min(cmp.Or(time.Duration(at.Max), MaxTTL), MaxTTL) if req.Duration > maxDur { return fmt.Errorf("requested duration %v exceeds max %v for access type %q", req.Duration, maxDur, at.Name) } diff --git a/hallpass_test.go b/hallpass_test.go index 99a0736..f3904c1 100644 --- a/hallpass_test.go +++ b/hallpass_test.go @@ -8,6 +8,7 @@ import ( "io" "log" "reflect" + "slices" "testing" "time" @@ -15,6 +16,45 @@ import ( "tailscale.com/tailcfg" ) +func TestDurationOptionsCap(t *testing.T) { + at := &accessTypes{ + Types: []accessTypeConfig{{ + Name: "Short", + Attr: "custom:short", + Max: timeDurationString(15 * time.Minute), + Default: timeDurationString(15 * time.Minute), + }, { + Name: "Long", + Attr: "custom:long", + Max: timeDurationString(168 * time.Hour), + Default: timeDurationString(1 * time.Hour), + }}, + } + var got []time.Duration + for _, opt := range at.DurationOptions() { + d, err := time.ParseDuration(opt.GoStr) + if err != nil { + t.Fatalf("parsing option %q: %v", opt.GoStr, err) + } + if d > MaxTTL { + t.Errorf("option %v exceeds MaxTTL %v", d, MaxTTL) + } + got = append(got, d) + } + want := []time.Duration{ + 15 * time.Minute, + 30 * time.Minute, + 1 * time.Hour, + 2 * time.Hour, + 4 * time.Hour, + 8 * time.Hour, + 12 * time.Hour, + } + if !slices.Equal(got, want) { + t.Errorf("DurationOptions() durations = %v, want %v", got, want) + } +} + func TestParseAccessTypeConfig(t *testing.T) { defer log.SetOutput(log.Default().Writer()) log.SetOutput(io.Discard)