@wtfalch/keys 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/bin/copy.d.ts +31 -0
- package/dist/bin/copy.js +61 -0
- package/dist/bin/migrations.d.ts +2 -0
- package/dist/bin/migrations.js +18 -0
- package/dist/held/aad.d.ts +13 -0
- package/dist/held/aad.js +25 -0
- package/dist/held/index.d.ts +134 -0
- package/dist/held/index.js +308 -0
- package/dist/held/schema.d.ts +278 -0
- package/dist/held/schema.js +43 -0
- package/dist/held/shred.d.ts +34 -0
- package/dist/held/shred.js +76 -0
- package/dist/issued/codec.d.ts +3 -0
- package/dist/issued/codec.js +7 -0
- package/dist/issued/encoding.d.ts +48 -0
- package/dist/issued/encoding.js +132 -0
- package/dist/issued/index.d.ts +88 -0
- package/dist/issued/index.js +273 -0
- package/dist/issued/secret.d.ts +30 -0
- package/dist/issued/secret.js +69 -0
- package/dist/issued/tables.d.ts +250 -0
- package/dist/issued/tables.js +35 -0
- package/dist/issued/verify.d.ts +8 -0
- package/dist/issued/verify.js +31 -0
- package/dist/migrations/0001_keys_issued.sql +80 -0
- package/dist/migrations/0002_keys_held.sql +60 -0
- package/dist/migrations/0003_keys_shred.sql +361 -0
- package/dist/worker-contract.d.ts +360 -0
- package/dist/worker-contract.js +94 -0
- package/package.json +52 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
-- @wtfalch/keys ./held: the crypto-shred sweep and its guard trigger (#232).
|
|
2
|
+
-- Mirrors src/held/schema.ts's tenant_archived_at and src/held/shred.ts.
|
|
3
|
+
--
|
|
4
|
+
-- Applied by the host's own migrate script after this file is copied into
|
|
5
|
+
-- its drizzle/ directory as the next number (keys-migrations); never edited
|
|
6
|
+
-- there. Every statement is idempotent. Purely additive: adds one column,
|
|
7
|
+
-- four functions, six triggers, one revoke and one grant. Nothing here
|
|
8
|
+
-- drops or renames anything 0002 created.
|
|
9
|
+
--
|
|
10
|
+
-- What "eligible" means (app-template #225, revised answer): a
|
|
11
|
+
-- keys_held_versions row's data key is destroyed once it has been eligible
|
|
12
|
+
-- for keys_shred_delay_days() (30, matching the estate's backup retention;
|
|
13
|
+
-- see #217's revised answer) -- retired past its own grace window, its entry
|
|
14
|
+
-- revoked, or its tenant archived, whichever came first. retired_at is set
|
|
15
|
+
-- once, by an AFTER INSERT trigger (this migration) when a new version
|
|
16
|
+
-- supersedes it; revoked_at already exists on keys_held_entries (0002,
|
|
17
|
+
-- revokeEntry); tenant_archived_at is the one new column this file adds,
|
|
18
|
+
-- set once by archiveTenant() across every entry a tenant holds -- this
|
|
19
|
+
-- package has no tenants table of its own, so "tenant archived" is
|
|
20
|
+
-- recorded per entry rather than on a row it doesn't own.
|
|
21
|
+
--
|
|
22
|
+
-- keys_shred_expired() is SECURITY DEFINER and takes no row-targeting
|
|
23
|
+
-- argument on purpose (#225's refutation, finding 1): a targeted
|
|
24
|
+
-- (entry_id, version) function would turn an ordinary EXECUTE grant into a
|
|
25
|
+
-- way to destroy a still-live secret on demand. This function recomputes
|
|
26
|
+
-- eligibility itself from retired_at / revoked_at / tenant_archived_at, so
|
|
27
|
+
-- calling it early or from a compromised process destroys nothing beyond
|
|
28
|
+
-- what was already due -- PROVIDED those three columns are trustworthy,
|
|
29
|
+
-- which is what the clock-stamping trigger below exists to guarantee.
|
|
30
|
+
--
|
|
31
|
+
-- Three separate guards protect three separate things:
|
|
32
|
+
--
|
|
33
|
+
-- 1. keys_held_versions_guard: every UPDATE on keys_held_versions must be
|
|
34
|
+
-- exactly a rewrap (a non-null wrapped_key replaced by another non-null
|
|
35
|
+
-- one, together with a kek_id change, nothing else different), a retire
|
|
36
|
+
-- (retired_at NULL to non-null, once), or a shred of a row genuinely
|
|
37
|
+
-- eligible for keys_shred_delay_days() (wrapped_key to NULL, kek_id and
|
|
38
|
+
-- retired_at unchanged). Everything else is refused, including
|
|
39
|
+
-- un-retiring, un-shredding (NULL back to a value), re-dating a retire,
|
|
40
|
+
-- and any change to tenant_id, entry_id, version, iv, ciphertext or
|
|
41
|
+
-- created_at.
|
|
42
|
+
--
|
|
43
|
+
-- Shape alone is not privilege: a real refutation on #232 (PR #8) proved
|
|
44
|
+
-- the runtime role could satisfy the "rewrap" shape with GARBAGE --
|
|
45
|
+
-- wrapped_key set to arbitrary bytes plus a real kek_id change -- because
|
|
46
|
+
-- Postgres has no way to verify a rewrapped key's *content*, only its
|
|
47
|
+
-- shape. That destroys the value at once (it can never be unwrapped
|
|
48
|
+
-- again) with no clock started and nothing for shredClocks to alert on --
|
|
49
|
+
-- worse than an early shred, which at least leaves a trace. The fix the
|
|
50
|
+
-- plan already specified (#225: "The runtime role gets EXECUTE, never
|
|
51
|
+
-- UPDATE") is privilege, not shape: the runtime role no longer has
|
|
52
|
+
-- UPDATE on this table AT ALL (see Grants) -- SELECT and INSERT only, so
|
|
53
|
+
-- rewrap and retire cannot be its own ordinary writes any more, and the
|
|
54
|
+
-- guard's shape checks now matter only for whoever the host trusts with
|
|
55
|
+
-- an operator handle (see item 3, Grants).
|
|
56
|
+
--
|
|
57
|
+
-- ciphertext and iv are refused here, matching the plan's own list.
|
|
58
|
+
-- 0002's held.test.ts constructs an AAD-mismatch fixture with a direct
|
|
59
|
+
-- UPDATE of exactly those two columns; that test runs its one fixture
|
|
60
|
+
-- statement with this trigger disabled (ALTER TABLE ... DISABLE
|
|
61
|
+
-- TRIGGER, as the migration owner), with a comment saying so.
|
|
62
|
+
-- Overwriting ciphertext/iv makes a value unopenable at once, the same
|
|
63
|
+
-- effect as an early shred, so there is no case where leaving them
|
|
64
|
+
-- unguarded is defensible.
|
|
65
|
+
--
|
|
66
|
+
-- 2. keys_held_versions_retire_predecessors / keys_held_entries_clock_guard:
|
|
67
|
+
-- keys_shred_delay_days() elapsing means nothing if the clock it
|
|
68
|
+
-- measures from can be forged. #225 revised: "The write that starts the
|
|
69
|
+
-- clock is itself an alert through the housekeeping tick, so an operator
|
|
70
|
+
-- can reverse a forged one before the shred fires" -- which only holds if
|
|
71
|
+
-- the clock's own timestamp cannot predate the write that set it. A
|
|
72
|
+
-- caller with ordinary INSERT/UPDATE privilege on these columns (the
|
|
73
|
+
-- runtime role, for revoked_at / tenant_archived_at; nobody, now, for
|
|
74
|
+
-- retired_at -- see item 3) could otherwise set one to a timestamp 31
|
|
75
|
+
-- days in the past in one statement, and the very next
|
|
76
|
+
-- keys_shred_expired() tick would shred it before any housekeeping-tick
|
|
77
|
+
-- alert had a chance to fire, since the alert (src/held/shred.ts's
|
|
78
|
+
-- shredClocks) only sees a clock once it exists, and this one would
|
|
79
|
+
-- already read as 31 days old the moment it appeared. Both guards close
|
|
80
|
+
-- this: on INSERT or UPDATE, a clock column moving from NULL to
|
|
81
|
+
-- non-null is stamped to now() regardless of what value the caller
|
|
82
|
+
-- sent, and once set, any further change to that same column -- a
|
|
83
|
+
-- different value, or NULL -- is refused outright. A clock can start
|
|
84
|
+
-- once, and only at the instant its own write actually ran.
|
|
85
|
+
--
|
|
86
|
+
-- 3. keys_held_versions_retire_predecessors: retired_at used to be set by
|
|
87
|
+
-- an ordinary UPDATE inside put() (src/held/index.ts) -- which needed
|
|
88
|
+
-- the runtime role to hold UPDATE on this table, the same privilege the
|
|
89
|
+
-- refutation exploited for a garbage rewrap. Retiring is now a SIDE
|
|
90
|
+
-- EFFECT of the INSERT put() already performs, not a second write: this
|
|
91
|
+
-- AFTER INSERT, SECURITY DEFINER trigger retires the entry's other
|
|
92
|
+
-- still-open versions itself, running as its owner rather than the
|
|
93
|
+
-- inserting role, so the runtime role never needs UPDATE at all. Its own
|
|
94
|
+
-- internal UPDATE still passes through keys_held_versions_guard (a
|
|
95
|
+
-- trigger fires regardless of who -- or what -- issued the statement),
|
|
96
|
+
-- lands on the "retire" shape, and gets the same now()-stamped,
|
|
97
|
+
-- set-once clock as any other write to retired_at.
|
|
98
|
+
--
|
|
99
|
+
-- Rewrap has no equivalent side-effect path: it is a deliberate,
|
|
100
|
+
-- infrequent operator sweep (#217), not something any INSERT or ordinary
|
|
101
|
+
-- runtime activity should trigger on its own. src/held/index.ts's
|
|
102
|
+
-- rewrap() stays in the API, documented as requiring an operator
|
|
103
|
+
-- database handle -- the owner or migration role, never the runtime
|
|
104
|
+
-- one. keys_held_versions_guard still limits even that role to the
|
|
105
|
+
-- rewrap shape, as a guard against mistakes, not against malice: an
|
|
106
|
+
-- operator is trusted with the privilege; the shape check just keeps a
|
|
107
|
+
-- fumbled UPDATE from doing something else instead.
|
|
108
|
+
--
|
|
109
|
+
-- Grants, following @wtfalch/audit's pattern (src/migrations/0001_audit.sql):
|
|
110
|
+
-- a host names its runtime role <database>_rt, and the DO block below is a
|
|
111
|
+
-- no-op if that role doesn't exist -- the functions and triggers still exist
|
|
112
|
+
-- and still protect the tables either way. The runtime role keeps its
|
|
113
|
+
-- ordinary default SELECT and INSERT on keys_held_versions (put and open
|
|
114
|
+
-- need them; nothing else does now) and its ordinary SELECT, INSERT, UPDATE
|
|
115
|
+
-- on keys_held_entries (revokeEntry, archiveTenant, and the shred guard's
|
|
116
|
+
-- own eligibility lookup all need them) -- this migration does not touch
|
|
117
|
+
-- keys_held_entries' grants at all. UPDATE, DELETE and TRUNCATE on
|
|
118
|
+
-- keys_held_versions are revoked from the runtime role: #225's revised
|
|
119
|
+
-- answer keeps a shredded row (permanent but unreadable, matching ADR 0019
|
|
120
|
+
-- and audit rows), so DELETE/TRUNCATE were never legitimate; UPDATE is
|
|
121
|
+
-- revoked because every legitimate write to this table -- retire, shred --
|
|
122
|
+
-- now runs through a SECURITY DEFINER function rather than the runtime
|
|
123
|
+
-- role's own privilege, and the one write that still needs ordinary UPDATE
|
|
124
|
+
-- (rewrap) is an operator's job, not the runtime role's (item 3). The
|
|
125
|
+
-- runtime role gets EXECUTE on keys_shred_expired(), nothing wider.
|
|
126
|
+
|
|
127
|
+
alter table keys_held_entries
|
|
128
|
+
add column if not exists tenant_archived_at timestamptz;
|
|
129
|
+
|
|
130
|
+
-- Shared by keys_shred_expired() and keys_held_versions_guard, so the sweep
|
|
131
|
+
-- and the guard's own re-check can never disagree about how long a clock
|
|
132
|
+
-- must run. IMMUTABLE: a pure constant, safe to inline by the planner.
|
|
133
|
+
create or replace function keys_shred_delay_days() returns integer
|
|
134
|
+
language sql immutable as $$
|
|
135
|
+
select 30;
|
|
136
|
+
$$;
|
|
137
|
+
|
|
138
|
+
create or replace function keys_shred_expired()
|
|
139
|
+
returns integer
|
|
140
|
+
language plpgsql
|
|
141
|
+
security definer
|
|
142
|
+
set search_path = pg_catalog, public
|
|
143
|
+
as $$
|
|
144
|
+
declare
|
|
145
|
+
shredded integer;
|
|
146
|
+
begin
|
|
147
|
+
update keys_held_versions kv
|
|
148
|
+
set wrapped_key = null
|
|
149
|
+
from keys_held_entries ke
|
|
150
|
+
where kv.tenant_id = ke.tenant_id
|
|
151
|
+
and kv.entry_id = ke.entry_id
|
|
152
|
+
and kv.wrapped_key is not null
|
|
153
|
+
and (
|
|
154
|
+
(kv.retired_at is not null
|
|
155
|
+
and now() >= kv.retired_at + (keys_shred_delay_days() || ' days')::interval)
|
|
156
|
+
or (ke.revoked_at is not null
|
|
157
|
+
and now() >= ke.revoked_at + (keys_shred_delay_days() || ' days')::interval)
|
|
158
|
+
or (ke.tenant_archived_at is not null
|
|
159
|
+
and now() >= ke.tenant_archived_at + (keys_shred_delay_days() || ' days')::interval)
|
|
160
|
+
);
|
|
161
|
+
get diagnostics shredded = row_count;
|
|
162
|
+
return shredded;
|
|
163
|
+
end
|
|
164
|
+
$$;
|
|
165
|
+
revoke all on function keys_shred_expired() from public;
|
|
166
|
+
|
|
167
|
+
create or replace function keys_held_versions_guard() returns trigger
|
|
168
|
+
language plpgsql as $$
|
|
169
|
+
declare
|
|
170
|
+
is_rewrap boolean;
|
|
171
|
+
is_retire boolean;
|
|
172
|
+
is_shred boolean;
|
|
173
|
+
begin
|
|
174
|
+
if tg_op = 'INSERT' then
|
|
175
|
+
-- Nothing that inserts a row -- put(), or keys_held_versions_retire_predecessors
|
|
176
|
+
-- below, which never inserts -- ever does so with retired_at set; this
|
|
177
|
+
-- guards a raw INSERT bypassing that, the same way the UPDATE branch
|
|
178
|
+
-- below guards a raw UPDATE.
|
|
179
|
+
if new.retired_at is not null then
|
|
180
|
+
new.retired_at := now();
|
|
181
|
+
end if;
|
|
182
|
+
return new;
|
|
183
|
+
elsif tg_op = 'DELETE' then
|
|
184
|
+
raise exception 'keys_held_versions is append-only: delete refused';
|
|
185
|
+
elsif tg_op = 'TRUNCATE' then
|
|
186
|
+
raise exception 'keys_held_versions is append-only: truncate refused';
|
|
187
|
+
elsif tg_op = 'UPDATE' then
|
|
188
|
+
-- Stamp: retired_at moving from NULL to non-null is forced to now(),
|
|
189
|
+
-- whatever the caller sent. Must run before the shape checks below, so
|
|
190
|
+
-- is_retire/is_shred see the real value, not a forged one.
|
|
191
|
+
if old.retired_at is null and new.retired_at is not null then
|
|
192
|
+
new.retired_at := now();
|
|
193
|
+
end if;
|
|
194
|
+
-- Once set, retired_at is fixed: no re-dating, no clearing.
|
|
195
|
+
if old.retired_at is not null and new.retired_at is distinct from old.retired_at then
|
|
196
|
+
raise exception 'keys_held_versions is append-only: retired_at may not be re-dated or cleared once set';
|
|
197
|
+
end if;
|
|
198
|
+
|
|
199
|
+
if new.tenant_id is distinct from old.tenant_id
|
|
200
|
+
or new.entry_id is distinct from old.entry_id
|
|
201
|
+
or new.version is distinct from old.version
|
|
202
|
+
or new.iv is distinct from old.iv
|
|
203
|
+
or new.ciphertext is distinct from old.ciphertext
|
|
204
|
+
or new.created_at is distinct from old.created_at
|
|
205
|
+
then
|
|
206
|
+
raise exception 'keys_held_versions is append-only: tenant_id, entry_id, version, iv, ciphertext and created_at may never change';
|
|
207
|
+
end if;
|
|
208
|
+
|
|
209
|
+
-- rewrap: an operator sweep (#217), run with an operator database
|
|
210
|
+
-- handle -- see the file header, item 3. The data key moves to a new
|
|
211
|
+
-- kek generation; wrapped_key and kek_id change together, retired_at
|
|
212
|
+
-- untouched either way. The guard cannot verify the new wrapped_key is
|
|
213
|
+
-- a genuine rewrap of the old one -- only Postgres-visible shape, never
|
|
214
|
+
-- content -- which is exactly why the runtime role no longer has
|
|
215
|
+
-- UPDATE at all (see Grants): this shape check is a guard against a
|
|
216
|
+
-- fumbled operator statement, not a security boundary on its own.
|
|
217
|
+
is_rewrap := old.wrapped_key is not null
|
|
218
|
+
and new.wrapped_key is not null
|
|
219
|
+
and new.wrapped_key is distinct from old.wrapped_key
|
|
220
|
+
and new.kek_id is distinct from old.kek_id
|
|
221
|
+
and new.retired_at is not distinct from old.retired_at;
|
|
222
|
+
|
|
223
|
+
-- retire: keys_held_versions_retire_predecessors, below, the only
|
|
224
|
+
-- thing that performs this write now. retired_at moves from NULL to a
|
|
225
|
+
-- value (now stamped, above), once, with nothing else about the row
|
|
226
|
+
-- changing.
|
|
227
|
+
is_retire := old.retired_at is null
|
|
228
|
+
and new.retired_at is not null
|
|
229
|
+
and new.wrapped_key is not distinct from old.wrapped_key
|
|
230
|
+
and new.kek_id is not distinct from old.kek_id;
|
|
231
|
+
|
|
232
|
+
-- shred (keys_shred_expired(), or an operator's own ordinary UPDATE
|
|
233
|
+
-- with the same shape -- the trigger cannot and does not distinguish
|
|
234
|
+
-- the two): the shape alone is not enough -- this row's own stored
|
|
235
|
+
-- clocks, which can no longer be forged (see the file header), must
|
|
236
|
+
-- already show keys_shred_delay_days() elapsed, the same predicate
|
|
237
|
+
-- keys_shred_expired()'s WHERE clause applies.
|
|
238
|
+
is_shred := old.wrapped_key is not null
|
|
239
|
+
and new.wrapped_key is null
|
|
240
|
+
and new.kek_id is not distinct from old.kek_id
|
|
241
|
+
and new.retired_at is not distinct from old.retired_at
|
|
242
|
+
and (
|
|
243
|
+
(new.retired_at is not null
|
|
244
|
+
and now() >= new.retired_at + (keys_shred_delay_days() || ' days')::interval)
|
|
245
|
+
or exists (
|
|
246
|
+
select 1 from keys_held_entries ke
|
|
247
|
+
where ke.tenant_id = new.tenant_id
|
|
248
|
+
and ke.entry_id = new.entry_id
|
|
249
|
+
and (
|
|
250
|
+
(ke.revoked_at is not null
|
|
251
|
+
and now() >= ke.revoked_at + (keys_shred_delay_days() || ' days')::interval)
|
|
252
|
+
or (ke.tenant_archived_at is not null
|
|
253
|
+
and now() >= ke.tenant_archived_at + (keys_shred_delay_days() || ' days')::interval)
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if not (is_rewrap or is_retire or is_shred) then
|
|
259
|
+
raise exception 'keys_held_versions is append-only: an update must be exactly a rewrap (wrapped_key and kek_id together), a retire (retired_at NULL to non-null, once), or a shred of a row genuinely eligible for keys_shred_delay_days() -- refused, including un-retiring, un-shredding, shredding early, and any change to ciphertext or iv';
|
|
260
|
+
end if;
|
|
261
|
+
end if;
|
|
262
|
+
return new;
|
|
263
|
+
end
|
|
264
|
+
$$;
|
|
265
|
+
drop trigger if exists keys_held_versions_stamp_insert on keys_held_versions;
|
|
266
|
+
create trigger keys_held_versions_stamp_insert
|
|
267
|
+
before insert on keys_held_versions
|
|
268
|
+
for each row execute function keys_held_versions_guard();
|
|
269
|
+
drop trigger if exists keys_held_versions_guarded_update on keys_held_versions;
|
|
270
|
+
create trigger keys_held_versions_guarded_update
|
|
271
|
+
before update on keys_held_versions
|
|
272
|
+
for each row execute function keys_held_versions_guard();
|
|
273
|
+
drop trigger if exists keys_held_versions_no_delete on keys_held_versions;
|
|
274
|
+
create trigger keys_held_versions_no_delete
|
|
275
|
+
before delete on keys_held_versions
|
|
276
|
+
for each row execute function keys_held_versions_guard();
|
|
277
|
+
drop trigger if exists keys_held_versions_no_truncate on keys_held_versions;
|
|
278
|
+
create trigger keys_held_versions_no_truncate
|
|
279
|
+
before truncate on keys_held_versions
|
|
280
|
+
for each statement execute function keys_held_versions_guard();
|
|
281
|
+
|
|
282
|
+
-- The retire-on-supersede write itself (file header, item 3): runs after
|
|
283
|
+
-- put()'s own INSERT commits, as this function's owner rather than the
|
|
284
|
+
-- inserting role, so the runtime role's INSERT privilege alone is enough to
|
|
285
|
+
-- retire a superseded version -- it never needs UPDATE. "Predecessors"
|
|
286
|
+
-- (plural) rather than "the previous version": retires every OTHER
|
|
287
|
+
-- still-open version of this entry, not just NEW.version - 1, so the
|
|
288
|
+
-- function stays correct even if versions were ever inserted out of order.
|
|
289
|
+
create or replace function keys_held_versions_retire_predecessors() returns trigger
|
|
290
|
+
language plpgsql
|
|
291
|
+
security definer
|
|
292
|
+
set search_path = pg_catalog, public
|
|
293
|
+
as $$
|
|
294
|
+
begin
|
|
295
|
+
update keys_held_versions
|
|
296
|
+
set retired_at = now()
|
|
297
|
+
where tenant_id = new.tenant_id
|
|
298
|
+
and entry_id = new.entry_id
|
|
299
|
+
and version <> new.version
|
|
300
|
+
and retired_at is null;
|
|
301
|
+
return null;
|
|
302
|
+
end
|
|
303
|
+
$$;
|
|
304
|
+
drop trigger if exists keys_held_versions_retire_predecessors_insert on keys_held_versions;
|
|
305
|
+
create trigger keys_held_versions_retire_predecessors_insert
|
|
306
|
+
after insert on keys_held_versions
|
|
307
|
+
for each row execute function keys_held_versions_retire_predecessors();
|
|
308
|
+
|
|
309
|
+
-- The keys_held_entries half of the same clock-stamping guarantee, for
|
|
310
|
+
-- revoked_at (0002) and tenant_archived_at (this file). Deliberately
|
|
311
|
+
-- narrower than keys_held_versions_guard: it says nothing about
|
|
312
|
+
-- current_version or any other column on this table, only that these two
|
|
313
|
+
-- clocks can never be backdated, re-dated, or cleared once set.
|
|
314
|
+
create or replace function keys_held_entries_clock_guard() returns trigger
|
|
315
|
+
language plpgsql as $$
|
|
316
|
+
begin
|
|
317
|
+
if tg_op = 'INSERT' then
|
|
318
|
+
if new.revoked_at is not null then
|
|
319
|
+
new.revoked_at := now();
|
|
320
|
+
end if;
|
|
321
|
+
if new.tenant_archived_at is not null then
|
|
322
|
+
new.tenant_archived_at := now();
|
|
323
|
+
end if;
|
|
324
|
+
return new;
|
|
325
|
+
elsif tg_op = 'UPDATE' then
|
|
326
|
+
if old.revoked_at is null and new.revoked_at is not null then
|
|
327
|
+
new.revoked_at := now();
|
|
328
|
+
end if;
|
|
329
|
+
if old.revoked_at is not null and new.revoked_at is distinct from old.revoked_at then
|
|
330
|
+
raise exception 'keys_held_entries: revoked_at may not be re-dated or cleared once set';
|
|
331
|
+
end if;
|
|
332
|
+
|
|
333
|
+
if old.tenant_archived_at is null and new.tenant_archived_at is not null then
|
|
334
|
+
new.tenant_archived_at := now();
|
|
335
|
+
end if;
|
|
336
|
+
if old.tenant_archived_at is not null and new.tenant_archived_at is distinct from old.tenant_archived_at then
|
|
337
|
+
raise exception 'keys_held_entries: tenant_archived_at may not be re-dated or cleared once set';
|
|
338
|
+
end if;
|
|
339
|
+
end if;
|
|
340
|
+
return new;
|
|
341
|
+
end
|
|
342
|
+
$$;
|
|
343
|
+
drop trigger if exists keys_held_entries_clock_stamp_insert on keys_held_entries;
|
|
344
|
+
create trigger keys_held_entries_clock_stamp_insert
|
|
345
|
+
before insert on keys_held_entries
|
|
346
|
+
for each row execute function keys_held_entries_clock_guard();
|
|
347
|
+
drop trigger if exists keys_held_entries_clock_guard_update on keys_held_entries;
|
|
348
|
+
create trigger keys_held_entries_clock_guard_update
|
|
349
|
+
before update on keys_held_entries
|
|
350
|
+
for each row execute function keys_held_entries_clock_guard();
|
|
351
|
+
|
|
352
|
+
do $$
|
|
353
|
+
declare
|
|
354
|
+
rt text := current_database() || '_rt';
|
|
355
|
+
begin
|
|
356
|
+
if exists (select 1 from pg_roles where rolname = rt) then
|
|
357
|
+
execute format('revoke update, delete, truncate on keys_held_versions from %I', rt);
|
|
358
|
+
execute format('grant execute on function keys_shred_expired() to %I', rt);
|
|
359
|
+
end if;
|
|
360
|
+
end
|
|
361
|
+
$$;
|