Skip to content
Closed
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
1 change: 0 additions & 1 deletion api/observability/v1/vocabulary.json
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,6 @@
"ready",
"expired",
"ready_limit",
"transfer_limit",
"cleanup",
"opened",
"closed",
Expand Down
2 changes: 1 addition & 1 deletion cmd/tnl/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func (o *publishOutput) ready(url string, publishRunNumber uint64) error {
)
}
if o.guestDemo {
fields = append(fields, clioutput.Field{Label: "guest trial", Value: "15 min / about 5 MiB across runs"})
fields = append(fields, clioutput.Field{Label: "guest trial", Value: "20 min ready across runs, including idle time"})
}
if o.fallbackPublicURL == publishRunNumber {
fields = append(fields, clioutput.Field{Label: "transport", Value: "TLS/TCP fallback"})
Expand Down
2 changes: 1 addition & 1 deletion cmd/tnl/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func TestGuestDemoReadyExplainsTrialAndSignIn(t *testing.T) {
if err := output.ready("https://demo-1.guest-01234567.tnl.dev", 1); err != nil {
t.Fatal(err)
}
if !strings.Contains(stderr.String(), "15 min / about 5 MiB across runs") ||
if !strings.Contains(stderr.String(), "20 min ready across runs, including idle time") ||
!strings.Contains(stderr.String(), "tnl auth login to publish your app") ||
!strings.Contains(stderr.String(), "demo-1.guest-01234567") {
t.Fatalf("guest ready output = %q", stderr.String())
Expand Down
5 changes: 5 additions & 0 deletions contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ baseline requires a fresh database and a coordinated stop/start of all serving
roles. Control checks both the exact migration version and baseline identity
before serving.

The coordinated peer-path rewrite keeps schema version 1 and uses baseline
`tnl-control-fresh-v1-peer-paths`. Stop every serving role, create and migrate a
fresh PostgreSQL database, then start the matching control, ingress, relay, and
publisher implementation together. The previous version-1 database is rejected.

The single PostgreSQL baseline includes durable publisher certificate-plan
checkpoints. Fresh identities have an empty plan inventory; submitted plans
remain required after order cleanup. See [publisher groups](docs/maintainers/publisher/publisher-groups.md).
Expand Down
2 changes: 1 addition & 1 deletion docs/maintainers/connector/platform-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ remain run-bound and use the configured readiness path. see
[compose publisher](../../../deploy/publisher/compose/readme.md).

the PostgreSQL baseline identity is
`tnl-control-fresh-v1-visitors`, at version 1. it adds verified
`tnl-control-fresh-v1-peer-paths`, at version 1. it adds verified
session bindings and removes the former HTTP-only credential storage. use a
fresh database and coordinated baseline rollout.

Expand Down
24 changes: 24 additions & 0 deletions docs/maintainers/control/guest-demos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# guest ready-time authority

guest demos have 20 cumulative minutes of ready authority, including idle time,
one active demo, and a 72-hour credential. control starts the interval only after
the publication is ready. it persists `ready_deadline` once from the remaining
budget and credential expiry; renewal cannot move that deadline.

publisher leases, ingress projections, and visitor-policy leases are capped at
the deadline. before readiness, a policy lease is conservatively capped by the
remaining budget from the observation time. ingress and publisher policy guards
close existing visitor connections locally when their lease expires, including
when control is unavailable.

`serving_until` records the latest authority issued to the active run. closing
the run settles its interval once through that horizon, capped by the ready
deadline. a restarted demo waits until that authority expires; closing a control
record alone cannot fence an offline process. late recovery charges no time after
the last issued horizon. this can charge the short outstanding lease tail after
a normal stop, rather than refund time that an offline owner might still use.

the trial's connection and byte-rate limits are server-owned. they do not track
cumulative transfer. guest source digests and the public URL's IP policy remain
independent of usage accounting. see [guest state](../../../internal/control/state/guests.go)
and [settlement queries](../../../internal/control/state/queries/guest_trials.sql).
2 changes: 1 addition & 1 deletion docs/maintainers/control/workload-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ verification before dialing.
## verify

the fresh PostgreSQL baseline and control's matching identity are
`tnl-control-fresh-v1-visitors`.
`tnl-control-fresh-v1-peer-paths`.
its connector constraint allows workload auth material through six hours;
current authorization remains bounded by the renewable lease. it requires the
same fresh-database, coordinated-baseline rollout as the assembled services work.
Expand Down
2 changes: 1 addition & 1 deletion docs/maintainers/publisher/publisher-provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ original signing identity. A newly provisioned identity needs a new binding;
there is no automatic takeover of an observed slot.

The fresh PostgreSQL baseline identity is now
`tnl-control-fresh-v1-visitors`.
`tnl-control-fresh-v1-peer-paths`.
This is part of the approved fresh-database rewrite, not an in-place migration.

## generated process environment
Expand Down
6 changes: 6 additions & 0 deletions internal/control/api/public/alias_browser_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ func TestIntegrationAliasBrowserHTTPAuthorizationAndLogout(t *testing.T) {

func TestIntegrationShareReviewerSignInRetainsShareVisitorPermission(t *testing.T) {
f := newCompanyHTTPFixture(t, "share_reviewer_browser")
if err := f.d.PrepareVisitorSigningKeys(t.Context(), f.server.URL, f.now); err != nil {
t.Fatal(err)
}
owner, reviewer := f.basic("share-owner"), f.basic("outside-reviewer")
m := owner.Identity.Memberships[0]
p := f.principal(owner)
Expand Down Expand Up @@ -191,6 +194,9 @@ func TestIntegrationShareReviewerSignInRetainsShareVisitorPermission(t *testing.
if err != nil {
t.Fatal(err)
}
if _, err := f.d.IssueVisitorToken(ctx, f.server.URL, visitorscope.Scope{Kind: visitorscope.PublicURL, ID: u.ID}, controlstate.ShareSessionTokenSource{ShareID: share.ID, Secret: shareSession.CookieSecret}, time.Now()); err != nil {
t.Fatal("redeemed share session did not prepare visitor permission", err)
}
handoff, err := f.d.IssueBrowserHandoff(ctx, controlstate.BrowserLoginAttempt{PreviewID: preview.ID, PublicURLID: u.ID, ReturnPath: "/"}, controlstate.BrowserAccessSession{
IdentityID: reviewer.Identity.Identity.Id, DisplayName: "Outside reviewer", AccessToken: reviewer.AccessToken, RefreshToken: reviewer.RefreshToken,
AccessExpiresAt: f.now.Add(30 * time.Minute), ExpiresAt: f.now.Add(time.Hour)}, f.now)
Expand Down
4 changes: 2 additions & 2 deletions internal/control/api/public/guest_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ func (a guestAuthorizer) Authorize(ctx context.Context, request authorization.Re
request.Operation == authorization.OperationFeedbackManage ||
request.Operation == authorization.OperationPreviewVisit ||
request.Operation != authorization.OperationPublicURLDelete &&
(!guest.ExpiresAt.After(time.Now()) || guest.UsedReady >= controlstate.GuestReadyAllowance || guest.UsedBytes >= controlstate.GuestByteAllowance) {
if !guest.ExpiresAt.After(time.Now()) || guest.UsedReady >= controlstate.GuestReadyAllowance || guest.UsedBytes >= controlstate.GuestByteAllowance {
(!guest.ExpiresAt.After(time.Now()) || guest.UsedReady >= controlstate.GuestReadyAllowance || !guest.ReadyDeadline.IsZero() && !guest.ReadyDeadline.After(time.Now())) {
if !guest.ExpiresAt.After(time.Now()) || guest.UsedReady >= controlstate.GuestReadyAllowance || !guest.ReadyDeadline.IsZero() && !guest.ReadyDeadline.After(time.Now()) {
return authorization.Decision{}, controlstate.ErrGuestTrialSpent
}
return authorization.Decision{}, authorization.ErrGuestDemoOnly
Expand Down
2 changes: 1 addition & 1 deletion internal/control/api/public/guest_authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func TestGuestAuthorizationLimitsPublicURLsToOneLocalDemo(t *testing.T) {
if _, err := authorizer.Authorize(t.Context(), create); !errors.Is(err, authorization.ErrForbidden) {
t.Fatalf("foreign public URL accepted: %v", err)
}
guest.UsedBytes = controlstate.GuestByteAllowance
guest.UsedReady = controlstate.GuestReadyAllowance
authorizer.store = guestStoreStub{guest: guest, owned: true}
if _, err := authorizer.Authorize(t.Context(), create); !errors.Is(err, controlstate.ErrGuestTrialSpent) {
t.Fatalf("exhausted guest accepted: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion internal/control/maintenance/guest_cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func RunGuestPrivateStateCleanup(ctx context.Context, store guestPrivateStateSto
stats, err := store.RecentGuestTrialStats(ctx, now)
if err == nil {
metrics.SetGuestTrialStats(stats.Issued, stats.Allocated, stats.Ready,
stats.Expired, stats.ReadyLimit, stats.TransferLimit)
stats.Expired, stats.ReadyLimit)
}
return count, err
}, func(count int, err error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/tnldotdev/tnl/internal/policy/visitorlease"
"github.com/tnldotdev/tnl/internal/policy/visitorscope"
"github.com/tnldotdev/tnl/internal/security/visitortoken"
"github.com/tnldotdev/tnl/pkg/protocol/tunnelv1"
)

Expand Down Expand Up @@ -130,6 +131,9 @@ func TestIntegrationPrivateVisitorCertificateDeadline(t *testing.T) {
t.Fatal(err)
}
header := tunnelv1.StreamReservation{ID: "sres_aaaaaaaaaaaaaaaaaaaaaa", AllocationID: initial.ID, Sequence: 1, ResourceKind: "service", ResourceID: run.ServiceID, RunID: run.RunID, RunNumber: uint64(run.RunNumber), VisitorConnectionID: "vc_aaaaaaaaaaaaaaaaaaaaaa", IngressID: ingress.IngressID, IngressRunID: ingress.IngressRunID, IngressLeaseRevision: ingress.IngressLeaseRevision}
if err := visitortoken.SignReservation(initial.ProofKey[:], &header, source); err != nil {
t.Fatal(err)
}
if err := local.ValidateReservation(header); err != nil {
t.Fatal("current prepared reservation denied", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/control/state/controlstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
const (
// the coordinated baseline requires the exact migration and identity.
schemaVersion int64 = 1
baselineID = "tnl-control-fresh-v1-visitors"
baselineID = "tnl-control-fresh-v1-peer-paths"
versionTable = "control.goose_db_version"
bootstrapRetryDelay = 25 * time.Millisecond
)
Expand Down
Loading
Loading