From cc85b35b8d2a860cb69ff3c45ee22b5c22777c05 Mon Sep 17 00:00:00 2001 From: Stefano Scafiti Date: Tue, 30 Jun 2026 11:20:34 +0200 Subject: [PATCH 1/4] Add per-script record TTL for written relationships Introduce an optional top-level `recordTtl` duration on scripts. When greater than zero, every relationship written by the script's WriteRelationships steps is given an expiration of `recordTtl` from the moment it is written (via Relationship.OptionalExpiresAt). The expiry is computed at write time so each relationship lives for the full TTL rather than expiring at a fixed prepare-time instant. Set recordTtl: 10m on some-data.yaml. --- README.md | 2 ++ internal/config/types.go | 7 ++++++ internal/config/types_test.go | 41 +++++++++++++++++++++++++++++++ internal/thumperrunner/prepare.go | 16 ++++++++++-- schema.yaml | 6 +++++ scripts/some-data.yaml | 1 + 6 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 internal/config/types_test.go diff --git a/README.md b/README.md index 79a87f1..3240bb0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Thumper config files are YAML files. These files support Go template preprocessi The final YAML generated by the templates must validate with the schema in [schema.yaml](schema.yaml). +A script may set an optional top-level `recordTtl`, a Go duration string (e.g. `30s`, `5m`). When `recordTtl` is greater than zero, every relationship written by the script's `WriteRelationships` steps is given an expiration that far in the future, computed at the moment each write is issued. Omitting `recordTtl` (or setting it to `0`) writes relationships with no expiration, as before. + #### Example ```yaml diff --git a/internal/config/types.go b/internal/config/types.go index 6662279..af90ba3 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "time" "github.com/goccy/go-yaml" "google.golang.org/protobuf/types/known/structpb" @@ -12,6 +13,12 @@ type Script struct { Name string Weight uint Steps []ScriptStep + + // RecordTTL, when greater than zero, sets an expiration on every + // relationship written by WriteRelationships steps in this script. Each + // written relationship expires RecordTTL after the moment it is written. + // Accepts Go duration strings, e.g. "30s" or "5m". + RecordTTL time.Duration `yaml:"recordTtl"` } // ScriptStep is a single step of a thumper script, for example a single call to CheckPermissions. diff --git a/internal/config/types_test.go b/internal/config/types_test.go new file mode 100644 index 0000000..9c9ef2e --- /dev/null +++ b/internal/config/types_test.go @@ -0,0 +1,41 @@ +package config + +import ( + "testing" + "time" + + "github.com/goccy/go-yaml" + "github.com/stretchr/testify/require" +) + +func TestScriptRecordTTL(t *testing.T) { + testCases := []struct { + name string + yaml string + expected time.Duration + }{ + { + name: "absent", + yaml: "name: s\nsteps: []\n", + expected: 0, + }, + { + name: "seconds", + yaml: "name: s\nrecordTtl: 30s\nsteps: []\n", + expected: 30 * time.Second, + }, + { + name: "minutes", + yaml: "name: s\nrecordTtl: 5m\nsteps: []\n", + expected: 5 * time.Minute, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var script Script + require.NoError(t, yaml.Unmarshal([]byte(tc.yaml), &script)) + require.Equal(t, tc.expected, script.RecordTTL) + }) + } +} diff --git a/internal/thumperrunner/prepare.go b/internal/thumperrunner/prepare.go index 2cd15f0..5050bf5 100644 --- a/internal/thumperrunner/prepare.go +++ b/internal/thumperrunner/prepare.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "strings" + "time" "github.com/authzed/internal/thumper/internal/config" @@ -15,6 +16,7 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" ) // Prepare transforms a loaded yaml script into one that can be efficiently executed. @@ -24,7 +26,7 @@ func Prepare(inputs []*config.Script) (prepared []*ExecutableScript, err error) for _, rawStep := range input.Steps { var step executableStep - step, err = prepareStep(rawStep) + step, err = prepareStep(rawStep, input.RecordTTL) if err != nil { return prepared, err } @@ -42,7 +44,7 @@ func Prepare(inputs []*config.Script) (prepared []*ExecutableScript, err error) return prepared, err } -func prepareStep(step config.ScriptStep) (executableStep, error) { +func prepareStep(step config.ScriptStep, recordTTL time.Duration) (executableStep, error) { consistencyForZedToken, consistencyDesc, err := prepareConsistency(step) if err != nil { return executableStep{}, fmt.Errorf("error preparing consistency: %w", err) @@ -215,6 +217,16 @@ func prepareStep(step config.ScriptStep) (executableStep, error) { } execStep.body = func(ctx context.Context, client *authzed.Client, _ *v1.ZedToken) (*v1.ZedToken, error) { + if recordTTL > 0 { + // Compute the expiration relative to write time so each + // written relationship lives for recordTTL rather than + // expiring at a fixed instant baked in at prepare time. + expiresAt := timestamppb.New(time.Now().Add(recordTTL)) + for _, update := range req.Updates { + update.Relationship.OptionalExpiresAt = expiresAt + } + } + resp, err := client.WriteRelationships(ctx, req) if err != nil { return nil, err diff --git a/schema.yaml b/schema.yaml index d5cea38..f743958 100644 --- a/schema.yaml +++ b/schema.yaml @@ -9,6 +9,12 @@ properties: weight: type: integer minimum: 1 + recordTtl: + type: string + description: >- + Go duration string (e.g. "30s", "5m"). When set, every relationship + written by WriteRelationships steps in this script expires this long + after it is written. steps: type: array minItems: 1 diff --git a/scripts/some-data.yaml b/scripts/some-data.yaml index 86b4869..68700e5 100644 --- a/scripts/some-data.yaml +++ b/scripts/some-data.yaml @@ -1,5 +1,6 @@ name: load some data (scattered keys) weight: 1 +recordTtl: 10m # randomObjectID returns ONE value per render, so on its own the only thing that # varied across the 100 entity groups below was the trailing "_" index. # Because object_id is the second column of the relation_tuple primary key, that From b965a43644244bf484633d53f4b7e18fbba605f1 Mon Sep 17 00:00:00 2001 From: Stefano Scafiti Date: Tue, 30 Jun 2026 12:49:52 +0200 Subject: [PATCH 2/4] Add expirable-schema.yaml with mandatory relationship expiration A variant of schema.yaml that declares `use expiration` and marks every relation `with expiration`, requiring a TTL on all writes against it. The script sets recordTtl: 10m, which the runner applies to every WriteRelationships step (including the seed step via the migrate/RunOnce path), so the seed fixtures are written with the same expiration. Verified the rendered schema compiles with the SpiceDB schema compiler. --- scripts/expirable-schema.yaml | 273 ++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 scripts/expirable-schema.yaml diff --git a/scripts/expirable-schema.yaml b/scripts/expirable-schema.yaml new file mode 100644 index 0000000..eee596b --- /dev/null +++ b/scripts/expirable-schema.yaml @@ -0,0 +1,273 @@ +name: write expirable thumper schema +# A variant of schema.yaml in which every relation requires an expiration (TTL) +# on its subjects, via SpiceDB's `use expiration` flag and the `with expiration` +# trait. Because every subject is declared `with expiration`, ALL writes against +# this schema MUST carry an OptionalExpiresAt -- including the seed writes below. +# recordTtl supplies that TTL: it is applied to every WriteRelationships step in +# this script (both the seed step here and any run-time script using this schema), +# so the seed relationships are written with the same expiration. +recordTtl: 10m +steps: +- op: WriteSchema + schema: | + use expiration + + definition {{ .Prefix }}resource { + relation writer: {{ .Prefix }}user with expiration + relation reader: {{ .Prefix }}user with expiration + permission edit = writer + permission view = reader + edit + } + + /** token represents a single token, assigned to a user or client */ + definition {{ .Prefix }}token { + /** owner is the owner of the token */ + relation owner: {{ .Prefix }}client with expiration | {{ .Prefix }}user with expiration + permission delete_token = owner + owner->delete_token + permission view_token = owner + owner->view_token + } + + /** revisionhash represents a revision hash of a schema for a tenant */ + definition {{ .Prefix }}revisionhash {} + + /** + * tenant represents a single tenant (permissions system) in the tenancy layer + */ + definition {{ .Prefix }}tenant { + /** organization is the organization that owns the tenant */ + relation organization: {{ .Prefix }}organization with expiration + + /** + * current_revision_hash is a link to the hash of the current schema for the tenant + */ + relation current_revision_hash: {{ .Prefix }}revisionhash with expiration + + /** + * admin is a user or client *token* that has the admin role on the tenant + */ + relation admin: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration + + /** + * writer is a user or client *token* that has the writer role on the tenant + */ + relation writer: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration + + /** + * viewer is a user or client *token* that has the viewer role on the tenant + */ + relation viewer: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration + + /** + * metrics_viewer is the client *token* that has the ability to view metrics for the tenant + */ + relation metrics_viewer: {{ .Prefix }}client#token with expiration + + // @deprecated + permission write = writer + admin_tenant + + // Tenants + permission admin_tenant = admin + organization->admin + permission update_tenant_metadata = admin_tenant + permission delete_tenant = admin_tenant + permission view_tenant = viewer + writer + admin_tenant + permission populate_tenant = write_schema & write_relationships + permission clone_tenant = read_schema & read_relationships + + // Metrics + permission view_metrics = metrics_viewer + view_tenant + + // Schema management + permission write_schema = admin_tenant + permission read_schema = view_tenant + + // Manage who has access to the tenant + permission view_access = admin_tenant + permission manage_access = admin_tenant + + // Relationships + permission write_relationships = writer + admin_tenant + permission delete_relationships = write_relationships + permission read_relationships = viewer + write_relationships + + // Permissions + permission check_permission = read_relationships + permission expand_permission_tree = read_relationships + permission lookup_resources = read_relationships + } + + /** platform is the root singleton for the tenancy platform */ + definition {{ .Prefix }}platform { + permission create_org = nil + } + + /** organization is an organization in the system */ + definition {{ .Prefix }}organization { + /** platform is the singleton platform */ + relation platform: {{ .Prefix }}platform with expiration + + /** + * prod_enabled links the organization to its own members, if creation of production tenants is allowed + */ + relation prod_enabled: {{ .Prefix }}organization with expiration + + /** + * admin is a user or client *token* that has the admin role on the organization + */ + relation admin: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration + + /** + * member is a user or client *token* that is a member of the organization + */ + relation member: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration + + // Orgs + permission admin_org = admin + permission delete_org = admin_org + permission update_org_metadata = admin_org + permission view_org = member + admin_org + + // Access + permission change_member_role = admin_org + permission invite_member = admin_org + permission delete_member = admin_org + permission view_members = view_org + permission is_member = member + admin + + // Clients + permission create_client = admin_org + + // Billing + permission request_production_access = admin_org + permission request_enhanced_support = admin_org + permission manage_billing = admin_org + permission view_billing = admin_org + + // Permissions Systems + permission create_dev_tenant = admin_org + permission create_prod_tenant = prod_enabled->create_dev_tenant + } + + /** + * namespace represents a single namespace defined in the schema of a tenant + */ + definition {{ .Prefix }}namespace { + relation tenant: {{ .Prefix }}tenant with expiration + } + + // @deprecated + definition {{ .Prefix }}flag { + relation enabled: {{ .Prefix }}organization with expiration + } + + /** user represents a user in the tenancy model */ + definition {{ .Prefix }}user { + relation token: {{ .Prefix }}token with expiration + } + + /** client represents a client in the tenancy model */ + definition {{ .Prefix }}client { + /** org is the organization that owns the client */ + relation org: {{ .Prefix }}organization with expiration + + /** token is a token owned by the client */ + relation token: {{ .Prefix }}token with expiration + + // Clients + permission admin_client = org->admin_org + permission delete_client = admin_client + permission view_client = org->view_org + admin_client + + // Tokens + permission create_token = admin_client + permission delete_token = admin_client + permission view_token = admin_client + } + + definition {{ .Prefix }}schemarevision {} + + /** schema represents a single schema for a tenant */ + definition {{ .Prefix }}schema { + relation revision: {{ .Prefix }}schemarevision with expiration + } +- op: WriteRelationships + updates: + - op: TOUCH + resource: {{ .Prefix }}resource:firstdoc + subject: {{ .Prefix }}user:tom + relation: writer + - op: TOUCH + resource: {{ .Prefix }}resource:firstdoc + subject: {{ .Prefix }}user:fred + relation: reader + - op: TOUCH + resource: {{ .Prefix }}resource:seconddoc + subject: {{ .Prefix }}user:tom + relation: reader + - op: TOUCH + resource: {{ .Prefix }}organization:firstorg + relation: admin + subject: {{ .Prefix }}user:fred#token + - op: TOUCH + resource: {{ .Prefix }}organization:secondorg + relation: admin + subject: {{ .Prefix }}user:jill#token + - op: TOUCH + resource: {{ .Prefix }}organization:secondorg + relation: member + subject: {{ .Prefix }}user:tom#token + - op: TOUCH + resource: {{ .Prefix }}organization:firstorg + relation: prod_enabled + subject: {{ .Prefix }}organization:firstorg + - op: TOUCH + resource: {{ .Prefix }}tenant:firsttenant + relation: organization + subject: {{ .Prefix }}organization:firstorg + - op: TOUCH + resource: {{ .Prefix }}tenant:secondtenant + relation: organization + subject: {{ .Prefix }}organization:secondorg + - op: TOUCH + resource: {{ .Prefix }}tenant:thirdtenant + relation: organization + subject: {{ .Prefix }}organization:secondorg + - op: TOUCH + resource: {{ .Prefix }}client:writerclient + relation: org + subject: {{ .Prefix }}organization:firstorg + - op: TOUCH + resource: {{ .Prefix }}client:writerclient + relation: token + subject: {{ .Prefix }}token:apptoken + - op: TOUCH + resource: {{ .Prefix }}tenant:firsttenant + relation: writer + subject: {{ .Prefix }}client:writerclient#token + - op: TOUCH + resource: {{ .Prefix }}token:apptoken + relation: owner + subject: {{ .Prefix }}client:writerclient + - op: TOUCH + resource: {{ .Prefix }}client:readclient + relation: org + subject: {{ .Prefix }}organization:secondorg + - op: TOUCH + resource: {{ .Prefix }}client:readclient + relation: token + subject: {{ .Prefix }}token:readertoken + - op: TOUCH + resource: {{ .Prefix }}tenant:secondtenant + relation: viewer + subject: {{ .Prefix }}client:readclient#token + - op: TOUCH + resource: {{ .Prefix }}token:readertoken + relation: owner + subject: {{ .Prefix }}client:readclient + - op: TOUCH + resource: {{ .Prefix }}tenant:thirdtenant + relation: admin + subject: {{ .Prefix }}user:sandra#token + - op: TOUCH + resource: {{ .Prefix }}tenant:thirdtenant + relation: viewer + subject: {{ .Prefix }}user:mike#token From bf125ec4596efc4d08a1b28b54cf2c33cd6f87e2 Mon Sep 17 00:00:00 2001 From: Stefano Scafiti Date: Tue, 30 Jun 2026 12:58:30 +0200 Subject: [PATCH 3/4] Make schema.yaml relations accept optional expiration Add `use expiration` and declare every relation in both plain and `with expiration` forms (e.g. `user | user with expiration`). This lets the same relation accept writes with or without a TTL: existing non-TTL writes still match the plain form, while scripts that set recordTtl (e.g. some-data.yaml) match the `with expiration` form. Removes the separate expirable-schema.yaml in favor of extending the existing schema. Verified the rendered schema compiles with the SpiceDB schema compiler. --- scripts/expirable-schema.yaml | 273 ---------------------------------- scripts/schema.yaml | 42 +++--- 2 files changed, 22 insertions(+), 293 deletions(-) delete mode 100644 scripts/expirable-schema.yaml diff --git a/scripts/expirable-schema.yaml b/scripts/expirable-schema.yaml deleted file mode 100644 index eee596b..0000000 --- a/scripts/expirable-schema.yaml +++ /dev/null @@ -1,273 +0,0 @@ -name: write expirable thumper schema -# A variant of schema.yaml in which every relation requires an expiration (TTL) -# on its subjects, via SpiceDB's `use expiration` flag and the `with expiration` -# trait. Because every subject is declared `with expiration`, ALL writes against -# this schema MUST carry an OptionalExpiresAt -- including the seed writes below. -# recordTtl supplies that TTL: it is applied to every WriteRelationships step in -# this script (both the seed step here and any run-time script using this schema), -# so the seed relationships are written with the same expiration. -recordTtl: 10m -steps: -- op: WriteSchema - schema: | - use expiration - - definition {{ .Prefix }}resource { - relation writer: {{ .Prefix }}user with expiration - relation reader: {{ .Prefix }}user with expiration - permission edit = writer - permission view = reader + edit - } - - /** token represents a single token, assigned to a user or client */ - definition {{ .Prefix }}token { - /** owner is the owner of the token */ - relation owner: {{ .Prefix }}client with expiration | {{ .Prefix }}user with expiration - permission delete_token = owner + owner->delete_token - permission view_token = owner + owner->view_token - } - - /** revisionhash represents a revision hash of a schema for a tenant */ - definition {{ .Prefix }}revisionhash {} - - /** - * tenant represents a single tenant (permissions system) in the tenancy layer - */ - definition {{ .Prefix }}tenant { - /** organization is the organization that owns the tenant */ - relation organization: {{ .Prefix }}organization with expiration - - /** - * current_revision_hash is a link to the hash of the current schema for the tenant - */ - relation current_revision_hash: {{ .Prefix }}revisionhash with expiration - - /** - * admin is a user or client *token* that has the admin role on the tenant - */ - relation admin: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration - - /** - * writer is a user or client *token* that has the writer role on the tenant - */ - relation writer: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration - - /** - * viewer is a user or client *token* that has the viewer role on the tenant - */ - relation viewer: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration - - /** - * metrics_viewer is the client *token* that has the ability to view metrics for the tenant - */ - relation metrics_viewer: {{ .Prefix }}client#token with expiration - - // @deprecated - permission write = writer + admin_tenant - - // Tenants - permission admin_tenant = admin + organization->admin - permission update_tenant_metadata = admin_tenant - permission delete_tenant = admin_tenant - permission view_tenant = viewer + writer + admin_tenant - permission populate_tenant = write_schema & write_relationships - permission clone_tenant = read_schema & read_relationships - - // Metrics - permission view_metrics = metrics_viewer + view_tenant - - // Schema management - permission write_schema = admin_tenant - permission read_schema = view_tenant - - // Manage who has access to the tenant - permission view_access = admin_tenant - permission manage_access = admin_tenant - - // Relationships - permission write_relationships = writer + admin_tenant - permission delete_relationships = write_relationships - permission read_relationships = viewer + write_relationships - - // Permissions - permission check_permission = read_relationships - permission expand_permission_tree = read_relationships - permission lookup_resources = read_relationships - } - - /** platform is the root singleton for the tenancy platform */ - definition {{ .Prefix }}platform { - permission create_org = nil - } - - /** organization is an organization in the system */ - definition {{ .Prefix }}organization { - /** platform is the singleton platform */ - relation platform: {{ .Prefix }}platform with expiration - - /** - * prod_enabled links the organization to its own members, if creation of production tenants is allowed - */ - relation prod_enabled: {{ .Prefix }}organization with expiration - - /** - * admin is a user or client *token* that has the admin role on the organization - */ - relation admin: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration - - /** - * member is a user or client *token* that is a member of the organization - */ - relation member: {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration - - // Orgs - permission admin_org = admin - permission delete_org = admin_org - permission update_org_metadata = admin_org - permission view_org = member + admin_org - - // Access - permission change_member_role = admin_org - permission invite_member = admin_org - permission delete_member = admin_org - permission view_members = view_org - permission is_member = member + admin - - // Clients - permission create_client = admin_org - - // Billing - permission request_production_access = admin_org - permission request_enhanced_support = admin_org - permission manage_billing = admin_org - permission view_billing = admin_org - - // Permissions Systems - permission create_dev_tenant = admin_org - permission create_prod_tenant = prod_enabled->create_dev_tenant - } - - /** - * namespace represents a single namespace defined in the schema of a tenant - */ - definition {{ .Prefix }}namespace { - relation tenant: {{ .Prefix }}tenant with expiration - } - - // @deprecated - definition {{ .Prefix }}flag { - relation enabled: {{ .Prefix }}organization with expiration - } - - /** user represents a user in the tenancy model */ - definition {{ .Prefix }}user { - relation token: {{ .Prefix }}token with expiration - } - - /** client represents a client in the tenancy model */ - definition {{ .Prefix }}client { - /** org is the organization that owns the client */ - relation org: {{ .Prefix }}organization with expiration - - /** token is a token owned by the client */ - relation token: {{ .Prefix }}token with expiration - - // Clients - permission admin_client = org->admin_org - permission delete_client = admin_client - permission view_client = org->view_org + admin_client - - // Tokens - permission create_token = admin_client - permission delete_token = admin_client - permission view_token = admin_client - } - - definition {{ .Prefix }}schemarevision {} - - /** schema represents a single schema for a tenant */ - definition {{ .Prefix }}schema { - relation revision: {{ .Prefix }}schemarevision with expiration - } -- op: WriteRelationships - updates: - - op: TOUCH - resource: {{ .Prefix }}resource:firstdoc - subject: {{ .Prefix }}user:tom - relation: writer - - op: TOUCH - resource: {{ .Prefix }}resource:firstdoc - subject: {{ .Prefix }}user:fred - relation: reader - - op: TOUCH - resource: {{ .Prefix }}resource:seconddoc - subject: {{ .Prefix }}user:tom - relation: reader - - op: TOUCH - resource: {{ .Prefix }}organization:firstorg - relation: admin - subject: {{ .Prefix }}user:fred#token - - op: TOUCH - resource: {{ .Prefix }}organization:secondorg - relation: admin - subject: {{ .Prefix }}user:jill#token - - op: TOUCH - resource: {{ .Prefix }}organization:secondorg - relation: member - subject: {{ .Prefix }}user:tom#token - - op: TOUCH - resource: {{ .Prefix }}organization:firstorg - relation: prod_enabled - subject: {{ .Prefix }}organization:firstorg - - op: TOUCH - resource: {{ .Prefix }}tenant:firsttenant - relation: organization - subject: {{ .Prefix }}organization:firstorg - - op: TOUCH - resource: {{ .Prefix }}tenant:secondtenant - relation: organization - subject: {{ .Prefix }}organization:secondorg - - op: TOUCH - resource: {{ .Prefix }}tenant:thirdtenant - relation: organization - subject: {{ .Prefix }}organization:secondorg - - op: TOUCH - resource: {{ .Prefix }}client:writerclient - relation: org - subject: {{ .Prefix }}organization:firstorg - - op: TOUCH - resource: {{ .Prefix }}client:writerclient - relation: token - subject: {{ .Prefix }}token:apptoken - - op: TOUCH - resource: {{ .Prefix }}tenant:firsttenant - relation: writer - subject: {{ .Prefix }}client:writerclient#token - - op: TOUCH - resource: {{ .Prefix }}token:apptoken - relation: owner - subject: {{ .Prefix }}client:writerclient - - op: TOUCH - resource: {{ .Prefix }}client:readclient - relation: org - subject: {{ .Prefix }}organization:secondorg - - op: TOUCH - resource: {{ .Prefix }}client:readclient - relation: token - subject: {{ .Prefix }}token:readertoken - - op: TOUCH - resource: {{ .Prefix }}tenant:secondtenant - relation: viewer - subject: {{ .Prefix }}client:readclient#token - - op: TOUCH - resource: {{ .Prefix }}token:readertoken - relation: owner - subject: {{ .Prefix }}client:readclient - - op: TOUCH - resource: {{ .Prefix }}tenant:thirdtenant - relation: admin - subject: {{ .Prefix }}user:sandra#token - - op: TOUCH - resource: {{ .Prefix }}tenant:thirdtenant - relation: viewer - subject: {{ .Prefix }}user:mike#token diff --git a/scripts/schema.yaml b/scripts/schema.yaml index bda7ffb..2ddf65e 100644 --- a/scripts/schema.yaml +++ b/scripts/schema.yaml @@ -1,10 +1,12 @@ name: write basic thumper schema steps: - op: WriteSchema - schema: | + schema: | + use expiration + definition {{ .Prefix }}resource { - relation writer: {{ .Prefix }}user - relation reader: {{ .Prefix }}user + relation writer: {{ .Prefix }}user | {{ .Prefix }}user with expiration + relation reader: {{ .Prefix }}user | {{ .Prefix }}user with expiration permission edit = writer permission view = reader + edit } @@ -12,7 +14,7 @@ steps: /** token represents a single token, assigned to a user or client */ definition {{ .Prefix }}token { /** owner is the owner of the token */ - relation owner: {{ .Prefix }}client | {{ .Prefix }}user + relation owner: {{ .Prefix }}client | {{ .Prefix }}user | {{ .Prefix }}client with expiration | {{ .Prefix }}user with expiration permission delete_token = owner + owner->delete_token permission view_token = owner + owner->view_token } @@ -25,32 +27,32 @@ steps: */ definition {{ .Prefix }}tenant { /** organization is the organization that owns the tenant */ - relation organization: {{ .Prefix }}organization + relation organization: {{ .Prefix }}organization | {{ .Prefix }}organization with expiration /** * current_revision_hash is a link to the hash of the current schema for the tenant */ - relation current_revision_hash: {{ .Prefix }}revisionhash + relation current_revision_hash: {{ .Prefix }}revisionhash | {{ .Prefix }}revisionhash with expiration /** * admin is a user or client *token* that has the admin role on the tenant */ - relation admin: {{ .Prefix }}user#token | {{ .Prefix }}client#token + relation admin: {{ .Prefix }}user#token | {{ .Prefix }}client#token | {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration /** * writer is a user or client *token* that has the writer role on the tenant */ - relation writer: {{ .Prefix }}user#token | {{ .Prefix }}client#token + relation writer: {{ .Prefix }}user#token | {{ .Prefix }}client#token | {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration /** * viewer is a user or client *token* that has the viewer role on the tenant */ - relation viewer: {{ .Prefix }}user#token | {{ .Prefix }}client#token + relation viewer: {{ .Prefix }}user#token | {{ .Prefix }}client#token | {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration /** * metrics_viewer is the client *token* that has the ability to view metrics for the tenant */ - relation metrics_viewer: {{ .Prefix }}client#token + relation metrics_viewer: {{ .Prefix }}client#token | {{ .Prefix }}client#token with expiration // @deprecated permission write = writer + admin_tenant @@ -93,22 +95,22 @@ steps: /** organization is an organization in the system */ definition {{ .Prefix }}organization { /** platform is the singleton platform */ - relation platform: {{ .Prefix }}platform + relation platform: {{ .Prefix }}platform | {{ .Prefix }}platform with expiration /** * prod_enabled links the organization to its own members, if creation of production tenants is allowed */ - relation prod_enabled: {{ .Prefix }}organization + relation prod_enabled: {{ .Prefix }}organization | {{ .Prefix }}organization with expiration /** * admin is a user or client *token* that has the admin role on the organization */ - relation admin: {{ .Prefix }}user#token | {{ .Prefix }}client#token + relation admin: {{ .Prefix }}user#token | {{ .Prefix }}client#token | {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration /** * member is a user or client *token* that is a member of the organization */ - relation member: {{ .Prefix }}user#token | {{ .Prefix }}client#token + relation member: {{ .Prefix }}user#token | {{ .Prefix }}client#token | {{ .Prefix }}user#token with expiration | {{ .Prefix }}client#token with expiration // Orgs permission admin_org = admin @@ -141,26 +143,26 @@ steps: * namespace represents a single namespace defined in the schema of a tenant */ definition {{ .Prefix }}namespace { - relation tenant: {{ .Prefix }}tenant + relation tenant: {{ .Prefix }}tenant | {{ .Prefix }}tenant with expiration } // @deprecated definition {{ .Prefix }}flag { - relation enabled: {{ .Prefix }}organization + relation enabled: {{ .Prefix }}organization | {{ .Prefix }}organization with expiration } /** user represents a user in the tenancy model */ definition {{ .Prefix }}user { - relation token: {{ .Prefix }}token + relation token: {{ .Prefix }}token | {{ .Prefix }}token with expiration } /** client represents a client in the tenancy model */ definition {{ .Prefix }}client { /** org is the organization that owns the client */ - relation org: {{ .Prefix }}organization + relation org: {{ .Prefix }}organization | {{ .Prefix }}organization with expiration /** token is a token owned by the client */ - relation token: {{ .Prefix }}token + relation token: {{ .Prefix }}token | {{ .Prefix }}token with expiration // Clients permission admin_client = org->admin_org @@ -177,7 +179,7 @@ steps: /** schema represents a single schema for a tenant */ definition {{ .Prefix }}schema { - relation revision: {{ .Prefix }}schemarevision + relation revision: {{ .Prefix }}schemarevision | {{ .Prefix }}schemarevision with expiration } - op: WriteRelationships updates: From a562258d467d82110bf5f820a36894b04a3b5aef Mon Sep 17 00:00:00 2001 From: Stefano Scafiti Date: Tue, 30 Jun 2026 13:03:26 +0200 Subject: [PATCH 4/4] Document recordTtl use case: bounded-disk write workloads Note that recordTtl is useful for sustained write-oriented load while keeping datastore disk usage bounded -- expired relationships are garbage-collected, so the store reaches a steady state instead of growing unboundedly. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3240bb0..f3854f0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ The final YAML generated by the templates must validate with the schema in [sche A script may set an optional top-level `recordTtl`, a Go duration string (e.g. `30s`, `5m`). When `recordTtl` is greater than zero, every relationship written by the script's `WriteRelationships` steps is given an expiration that far in the future, computed at the moment each write is issued. Omitting `recordTtl` (or setting it to `0`) writes relationships with no expiration, as before. +This is useful for driving sustained write-oriented workloads while keeping disk usage bounded: as the generator keeps writing new relationships, older ones expire and are garbage-collected, so the datastore reaches a steady state instead of growing without limit. Note that relations targeted by a TTL write must permit expiration in the schema (declared `with expiration`); see [schema.yaml](schema.yaml), where every relation accepts both expiring and non-expiring writes. + #### Example ```yaml