Skip to content

Commit 85d10c9

Browse files
committed
add secrets-meta package
1 parent f6bbdfb commit 85d10c9

13 files changed

Lines changed: 405 additions & 0 deletions

File tree

packages/secrets-meta/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# secrets-meta
2+
3+
Meta-level secrets metadata module for PGPM:
4+
5+
- `meta_public.secret_providers` — registry of secret backends (OpenBao, k8s, etc.)
6+
- `meta_public.secrets` — per-owner/app secret metadata (no values)
7+
- helper functions for metadata management and job→secret metadata lookup.
8+
9+
Values are stored in an external provider (e.g. OpenBao KV v2); this
10+
module stores only the routing and ownership information.
11+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
-- Deploy schemas/meta_private/procedures/get_job_secrets_metadata to pg
2+
-- requires: schemas/meta_private/schema
3+
-- requires: pgpm-database-jobs:schemas/app_jobs/tables/jobs/table
4+
-- requires: schemas/meta_public/tables/secrets/table
5+
-- requires: schemas/meta_public/tables/secret_providers/table
6+
-- requires: db-meta-schema:schemas/meta_public/tables/apps/table
7+
8+
BEGIN;
9+
10+
CREATE OR REPLACE FUNCTION meta_private.get_job_secrets_metadata(
11+
in_job_id bigint
12+
)
13+
RETURNS TABLE (
14+
secret_id uuid,
15+
key text,
16+
provider_type text,
17+
provider_config jsonb,
18+
provider_ref text,
19+
app_id uuid,
20+
database_id uuid,
21+
task_identifier text
22+
)
23+
LANGUAGE sql
24+
SECURITY DEFINER
25+
AS $$
26+
WITH job_row AS (
27+
SELECT
28+
j.id,
29+
j.database_id,
30+
j.task_identifier,
31+
j.payload
32+
FROM app_jobs.jobs j
33+
WHERE j.id = in_job_id
34+
),
35+
refs AS (
36+
SELECT
37+
jr.database_id,
38+
jr.task_identifier,
39+
(each_ref).key AS ref_key,
40+
(each_ref).value AS ref_value
41+
FROM job_row jr,
42+
LATERAL json_each(jr.payload -> 'secretRefs') AS each_ref(key, value)
43+
),
44+
resolved AS (
45+
SELECT
46+
s.id AS secret_id,
47+
s.key,
48+
sp.provider_type,
49+
sp.config AS provider_config,
50+
s.provider_ref,
51+
s.app_id,
52+
a.database_id,
53+
r.task_identifier
54+
FROM refs r
55+
JOIN meta_public.secrets s
56+
ON s.owner_type = (r.ref_value ->> 'ownerType')
57+
AND s.owner_id = (r.ref_value ->> 'ownerId')::uuid
58+
AND s.app_id = (r.ref_value ->> 'appId')::uuid
59+
AND s.key_normalized = lower(r.ref_value ->> 'key')
60+
JOIN meta_public.secret_providers sp
61+
ON sp.id = s.provider_id
62+
JOIN meta_public.apps a
63+
ON a.id = s.app_id
64+
AND a.database_id = r.database_id
65+
WHERE sp.is_active
66+
)
67+
SELECT
68+
secret_id,
69+
key,
70+
provider_type,
71+
provider_config,
72+
provider_ref,
73+
app_id,
74+
database_id,
75+
task_identifier
76+
FROM resolved;
77+
$$;
78+
79+
COMMIT;
80+
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
-- Deploy schemas/meta_public/procedures/secret_metadata to pg
2+
-- requires: schemas/meta_public/tables/secrets/table
3+
4+
BEGIN;
5+
6+
CREATE OR REPLACE FUNCTION meta_public.create_secret_metadata(
7+
in_owner_type text,
8+
in_owner_id uuid,
9+
in_app_id uuid,
10+
in_key text,
11+
in_provider_id uuid,
12+
in_provider_ref text,
13+
in_description text DEFAULT NULL
14+
)
15+
RETURNS meta_public.secrets
16+
LANGUAGE plpgsql
17+
SECURITY DEFINER
18+
AS $$
19+
DECLARE
20+
v_secret meta_public.secrets;
21+
BEGIN
22+
IF in_owner_type NOT IN ('user', 'org', 'app', 'site') THEN
23+
RAISE EXCEPTION 'invalid owner_type %', in_owner_type;
24+
END IF;
25+
26+
INSERT INTO meta_public.secrets (
27+
owner_type,
28+
owner_id,
29+
app_id,
30+
key,
31+
provider_id,
32+
provider_ref,
33+
description
34+
) VALUES (
35+
in_owner_type,
36+
in_owner_id,
37+
in_app_id,
38+
in_key,
39+
in_provider_id,
40+
in_provider_ref,
41+
in_description
42+
)
43+
RETURNING * INTO v_secret;
44+
45+
RETURN v_secret;
46+
END;
47+
$$;
48+
49+
50+
CREATE OR REPLACE FUNCTION meta_public.rotate_secret_metadata(
51+
in_secret_id uuid
52+
)
53+
RETURNS meta_public.secrets
54+
LANGUAGE plpgsql
55+
SECURITY DEFINER
56+
AS $$
57+
DECLARE
58+
v_secret meta_public.secrets;
59+
BEGIN
60+
UPDATE meta_public.secrets s
61+
SET rotated_at = current_timestamp,
62+
updated_at = current_timestamp
63+
WHERE s.id = in_secret_id
64+
RETURNING * INTO v_secret;
65+
66+
IF NOT FOUND THEN
67+
RAISE EXCEPTION 'secret % not found', in_secret_id;
68+
END IF;
69+
70+
RETURN v_secret;
71+
END;
72+
$$;
73+
74+
75+
CREATE OR REPLACE FUNCTION meta_public.delete_secret_metadata(
76+
in_secret_id uuid
77+
)
78+
RETURNS void
79+
LANGUAGE plpgsql
80+
SECURITY DEFINER
81+
AS $$
82+
BEGIN
83+
DELETE FROM meta_public.secrets s
84+
WHERE s.id = in_secret_id;
85+
END;
86+
$$;
87+
88+
COMMIT;
89+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-- Deploy schemas/meta_public/tables/secret_providers/table to pg
2+
-- requires: schemas/meta_public/schema
3+
4+
BEGIN;
5+
6+
CREATE TABLE IF NOT EXISTS meta_public.secret_providers (
7+
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
8+
name text NOT NULL,
9+
provider_type text NOT NULL,
10+
config jsonb NOT NULL DEFAULT '{}'::jsonb,
11+
description text,
12+
is_active boolean NOT NULL DEFAULT true,
13+
created_at timestamptz NOT NULL DEFAULT current_timestamp,
14+
updated_at timestamptz NOT NULL DEFAULT current_timestamp
15+
);
16+
17+
COMMENT ON TABLE meta_public.secret_providers IS
18+
'Registry of secret provider backends (OpenBao, k8s, etc).';
19+
20+
COMMENT ON COLUMN meta_public.secret_providers.name IS
21+
'Human-readable name for this secret provider.';
22+
23+
COMMENT ON COLUMN meta_public.secret_providers.provider_type IS
24+
'Provider type identifier (e.g. openbao, k8s, aws_secrets_manager).';
25+
26+
COMMENT ON COLUMN meta_public.secret_providers.config IS
27+
'Provider-specific configuration (JSON), such as endpoints, mounts, roles.';
28+
29+
COMMENT ON COLUMN meta_public.secret_providers.is_active IS
30+
'Whether this provider is currently active/usable.';
31+
32+
COMMIT;
33+
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
-- Deploy schemas/meta_public/tables/secrets/table to pg
2+
-- requires: schemas/meta_public/schema
3+
-- requires: schemas/meta_public/tables/apps/table
4+
-- requires: schemas/meta_public/tables/secret_providers/table
5+
6+
BEGIN;
7+
8+
CREATE TABLE IF NOT EXISTS meta_public.secrets (
9+
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
10+
11+
-- Ownership / scope
12+
owner_type text NOT NULL, -- user | org | app | site
13+
owner_id uuid NOT NULL,
14+
app_id uuid NOT NULL,
15+
16+
-- Logical key
17+
key text NOT NULL,
18+
19+
-- Normalized key for uniqueness (lowercased or citext)
20+
key_normalized text NOT NULL,
21+
22+
-- Provider linkage
23+
provider_id uuid NOT NULL,
24+
provider_ref text NOT NULL,
25+
26+
description text,
27+
28+
is_active boolean NOT NULL DEFAULT true,
29+
30+
created_at timestamptz NOT NULL DEFAULT current_timestamp,
31+
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
32+
rotated_at timestamptz
33+
);
34+
35+
COMMENT ON TABLE meta_public.secrets IS
36+
'Metadata for user/org/app secrets; values live in external providers.';
37+
38+
COMMENT ON COLUMN meta_public.secrets.owner_type IS
39+
'Owner type for the secret: user, org, app, or site.';
40+
41+
COMMENT ON COLUMN meta_public.secrets.owner_id IS
42+
'ID of the owning user/org/app/site.';
43+
44+
COMMENT ON COLUMN meta_public.secrets.app_id IS
45+
'Logical app/database this secret is associated with.';
46+
47+
COMMENT ON COLUMN meta_public.secrets.key IS
48+
'Logical secret key name (e.g. MAILGUN_API_KEY).';
49+
50+
COMMENT ON COLUMN meta_public.secrets.key_normalized IS
51+
'Normalized form of key used for uniqueness (e.g. lower(key)).';
52+
53+
COMMENT ON COLUMN meta_public.secrets.provider_id IS
54+
'Foreign key to meta_public.secret_providers.';
55+
56+
COMMENT ON COLUMN meta_public.secrets.provider_ref IS
57+
'Opaque provider-specific reference/path (e.g. OpenBao KV path).';
58+
59+
ALTER TABLE meta_public.secrets
60+
ADD CONSTRAINT secrets_app_fkey
61+
FOREIGN KEY (app_id)
62+
REFERENCES meta_public.apps (id)
63+
ON DELETE CASCADE;
64+
65+
ALTER TABLE meta_public.secrets
66+
ADD CONSTRAINT secrets_provider_fkey
67+
FOREIGN KEY (provider_id)
68+
REFERENCES meta_public.secret_providers (id)
69+
ON DELETE RESTRICT;
70+
71+
CREATE UNIQUE INDEX IF NOT EXISTS secrets_owner_app_key_norm_uniq
72+
ON meta_public.secrets (owner_type, owner_id, app_id, key_normalized);
73+
74+
COMMIT;
75+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- Deploy schemas/meta_public/tables/secrets/triggers/invariants to pg
2+
-- requires: schemas/meta_public/tables/secrets/table
3+
4+
BEGIN;
5+
6+
CREATE OR REPLACE FUNCTION meta_public.secrets_invariants_tg()
7+
RETURNS trigger AS $$
8+
BEGIN
9+
IF TG_OP = 'INSERT' THEN
10+
-- Normalize key
11+
NEW.key_normalized := lower(NEW.key);
12+
13+
-- Timestamps
14+
IF NEW.created_at IS NULL THEN
15+
NEW.created_at := current_timestamp;
16+
END IF;
17+
IF NEW.updated_at IS NULL THEN
18+
NEW.updated_at := current_timestamp;
19+
END IF;
20+
21+
ELSIF TG_OP = 'UPDATE' THEN
22+
-- Normalize key
23+
NEW.key_normalized := lower(NEW.key);
24+
25+
-- Prevent provider_ref changes
26+
IF NEW.provider_ref IS DISTINCT FROM OLD.provider_ref THEN
27+
RAISE EXCEPTION 'provider_ref is immutable for secret %', OLD.id;
28+
END IF;
29+
30+
-- Maintain updated_at
31+
NEW.updated_at := current_timestamp;
32+
END IF;
33+
34+
RETURN NEW;
35+
END;
36+
$$ LANGUAGE plpgsql VOLATILE;
37+
38+
DROP TRIGGER IF EXISTS secrets_invariants_before_tg ON meta_public.secrets;
39+
40+
CREATE TRIGGER secrets_invariants_before_tg
41+
BEFORE INSERT OR UPDATE ON meta_public.secrets
42+
FOR EACH ROW
43+
EXECUTE PROCEDURE meta_public.secrets_invariants_tg();
44+
45+
COMMIT;
46+

packages/secrets-meta/pgpm.plan

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
%syntax-version=1.0.0
2+
%project=secrets-meta
3+
%uri=secrets-meta
4+
5+
# Meta-level secrets metadata and helpers
6+
7+
schemas/meta_public/tables/secret_providers/table [db-meta-schema:schemas/meta_public/tables/apps/table] 2025-01-01T00:00:00Z secrets-meta <secrets@constructive.io> # add secret_providers registry
8+
schemas/meta_public/tables/secrets/table [schemas/meta_public/tables/secret_providers/table db-meta-schema:schemas/meta_public/tables/apps/table] 2025-01-01T00:00:00Z secrets-meta <secrets@constructive.io> # add secrets metadata table
9+
10+
schemas/meta_public/tables/secrets/triggers/invariants [schemas/meta_public/tables/secrets/table] 2025-01-01T00:00:00Z secrets-meta <secrets@constructive.io> # enforce invariants on secrets
11+
12+
schemas/meta_public/procedures/secret_metadata [schemas/meta_public/tables/secrets/table] 2025-01-01T00:00:00Z secrets-meta <secrets@constructive.io> # helpers for create/rotate/delete metadata
13+
14+
schemas/meta_private/procedures/get_job_secrets_metadata [pgpm-database-jobs:schemas/app_jobs/tables/jobs/table schemas/meta_public/tables/secrets/table schemas/meta_public/tables/secret_providers/table db-meta-schema:schemas/meta_public/tables/apps/table] 2025-01-01T00:00:00Z secrets-meta <secrets@constructive.io> # helper for jobId→secretRefs→metadata
15+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Revert schemas/meta_private/procedures/get_job_secrets_metadata from pg
2+
3+
BEGIN;
4+
5+
DROP FUNCTION IF EXISTS meta_private.get_job_secrets_metadata(bigint);
6+
7+
COMMIT;
8+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Revert schemas/meta_public/procedures/secret_metadata from pg
2+
3+
BEGIN;
4+
5+
DROP FUNCTION IF EXISTS meta_public.delete_secret_metadata(uuid);
6+
DROP FUNCTION IF EXISTS meta_public.rotate_secret_metadata(uuid);
7+
DROP FUNCTION IF EXISTS meta_public.create_secret_metadata(
8+
text,
9+
uuid,
10+
uuid,
11+
text,
12+
uuid,
13+
text,
14+
text
15+
);
16+
17+
COMMIT;
18+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Revert schemas/meta_public/tables/secret_providers/table from pg
2+
3+
BEGIN;
4+
5+
DROP TABLE IF EXISTS meta_public.secret_providers CASCADE;
6+
7+
COMMIT;
8+

0 commit comments

Comments
 (0)