mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 15:44:33 +00:00
knoe-auth (prole.org k3s): - Fix CNPG manifest drift: remove spec.backup.pluginConfiguration (CNPG 1.28 only), switch spec.certificates from serverTLSSecret to serverAltDNSNames - Apply knoe-auth Round 1 schema + GRANTs manually (postInitSQL had never run on live cluster) - Fix OIDC signing key generator: base64(DER) not base64(PEM) — OidcTokenService does Base64.decode() → PKCS8EncodedKeySpec which requires raw DER bytes - Add OIDC controllers: authorize, token, userinfo, jwks, discovery - Add prole Spring profile: cookieDomain, emailDomain, Kerberos config - Add secret example templates: knoe-db-user, knoe-auth-oidc-signing, knoe-auth-google-prole - Kong configmap: scope knoe-auth route to /auth prefix only Tenant onboarding: - Add etc/onboard_tenant.sh: provision/apply/rotate/status workflow backed by 1Password vaults; types: 'enterprise' (own Kerberos + domain) and 'tenant' (hosted, initContainer KDC) - Provision 'Knoe Tenant - prole.org' vault; apply all 7 k8s secrets to knoe-system - init_knoe_auth.sh: add explicit GRANT + ALTER DEFAULT PRIVILEGES for knoe role Cluster stabilisation: - gitea: roll back 14-day stuck rollout (RWO PVC + maxSurge=100% deadlock); patch deployment strategy to Recreate - supabase: create supabase_admin role, _supabase db, _analytics schema, _realtime schema in CNPG — analytics and realtime had never connected since Helm install day 1 - knoe-db barman ObjectStore: add GCS-backed objectstore manifest + scheduled backup Infrastructure: - gandalf host_vars: k3s registry config - pi host_vars: clean up stale entries - knoe-db schemas: ekosystem.sql, ekosystem_objects.sql - init_prole_app.sql: prole app DB initialisation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
279 lines
11 KiB
PL/PgSQL
279 lines
11 KiB
PL/PgSQL
-- =============================================================================
|
|
-- knoe.ekosystem_objects — Modular object kind registry, naming, and
|
|
-- cross-ekosystem sharing / grant model
|
|
--
|
|
-- Depends on: ekosystem.sql (knoe.tenants, knoe.ekosystem_decode)
|
|
--
|
|
-- Object naming convention:
|
|
-- {prefix}_{uuid} — standalone objects (project, user, embedding)
|
|
-- {prefix}_{uuid}_{name} — compound objects (table, index, view)
|
|
--
|
|
-- All prefixes are {letter}_ so ekosystem_tenant() works uniformly:
|
|
-- substring(object_name FROM 3 FOR 16) always extracts the UUID.
|
|
--
|
|
-- Cross-ekosystem sharing:
|
|
-- UUIDs embed the originating tenant_id in bits — ownership is always
|
|
-- decodable without a registry join, so imported objects cannot collide
|
|
-- even when mixed across ekosystems. knoe.cross_grants records the
|
|
-- explicit sharing approvals that LDAP/Samba AD will eventually enforce.
|
|
-- =============================================================================
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.object_kinds — Modular kind registry
|
|
-- Add new kinds here; ekosystem_object() picks them up automatically.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS knoe.object_kinds (
|
|
kind text PRIMARY KEY,
|
|
prefix char(1) NOT NULL UNIQUE CHECK (prefix ~ '^[a-z]$'),
|
|
description text NOT NULL
|
|
);
|
|
|
|
INSERT INTO knoe.object_kinds (kind, prefix, description) VALUES
|
|
('project', 'p', 'Project namespace — schema or resource root'),
|
|
('user', 'u', 'Service account / PostgreSQL role'),
|
|
('embedding', 'e', 'Vector embedding store'),
|
|
('table', 't', 'Data table'),
|
|
('index', 'x', 'Index or search structure'),
|
|
('view', 'v', 'Logical view or materialised view'),
|
|
('sequence', 's', 'Auto-increment sequence'),
|
|
('function', 'f', 'Stored function or procedure')
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
GRANT SELECT ON TABLE knoe.object_kinds TO knoe;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.ekosystem_object(uuid, kind) → '{prefix}_{uuid}'
|
|
--
|
|
-- Returns the canonical identifier for any typed object.
|
|
-- Unknown kind raises — register it in knoe.object_kinds first.
|
|
--
|
|
-- Compound names (tables, indexes):
|
|
-- knoe.ekosystem_object(uuid, 'table') || '_accounts'
|
|
-- → 't_3k9mxqt2f8vn0r7b_accounts'
|
|
--
|
|
-- The 2-char prefix is the same width for every kind, so
|
|
-- ekosystem_tenant() always finds the UUID at position 3.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION knoe.ekosystem_object(p_uuid text, p_kind text)
|
|
RETURNS text
|
|
LANGUAGE plpgsql STABLE
|
|
AS $$
|
|
DECLARE
|
|
v_prefix char(1);
|
|
BEGIN
|
|
SELECT prefix INTO v_prefix FROM knoe.object_kinds WHERE kind = p_kind;
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION
|
|
'Unknown object kind: %. Register it in knoe.object_kinds first.', p_kind;
|
|
END IF;
|
|
RETURN v_prefix || '_' || p_uuid;
|
|
END;
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.ekosystem_kind(object_name) → kind
|
|
--
|
|
-- Reverse lookup: given any prefixed object name, return its kind.
|
|
-- Returns NULL if the prefix is unregistered (not an error — enables routing
|
|
-- filters that skip non-ekosystem objects).
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION knoe.ekosystem_kind(p_object_name text)
|
|
RETURNS text
|
|
LANGUAGE sql STABLE
|
|
AS $$
|
|
SELECT kind
|
|
FROM knoe.object_kinds
|
|
WHERE prefix = substring(p_object_name FROM 1 FOR 1);
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.cross_grants — Cross-ekosystem sharing policy
|
|
--
|
|
-- Records that grantor_tenant has shared an object with grantee_tenant.
|
|
-- This table is the policy source of truth; actual PG GRANT statements
|
|
-- and future LDAP/Samba AD group memberships are derived from it.
|
|
--
|
|
-- Ownership is always decodable from the UUID itself via ekosystem_decode(),
|
|
-- so this table only needs to record the *exceptions* (sharing across
|
|
-- tenant boundaries). Within a single ekosystem, use standard PG GRANT.
|
|
--
|
|
-- privileges: PostgreSQL privilege names, e.g. '{SELECT}', '{SELECT,INSERT}'
|
|
-- expires_at: NULL = permanent until explicitly revoked
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS knoe.cross_grants (
|
|
id text PRIMARY KEY
|
|
DEFAULT knoe.ekosystem_id(0, 0),
|
|
object_uuid text NOT NULL,
|
|
object_kind text NOT NULL REFERENCES knoe.object_kinds(kind),
|
|
grantor_tenant smallint NOT NULL REFERENCES knoe.tenants(tenant_id),
|
|
grantee_tenant smallint NOT NULL REFERENCES knoe.tenants(tenant_id),
|
|
privileges text[] NOT NULL CHECK (array_length(privileges, 1) > 0),
|
|
granted_at timestamptz NOT NULL DEFAULT now(),
|
|
expires_at timestamptz,
|
|
CONSTRAINT cross_grants_no_self
|
|
CHECK (grantor_tenant != grantee_tenant),
|
|
CONSTRAINT cross_grants_unique
|
|
UNIQUE (object_uuid, object_kind, grantee_tenant)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS cross_grants_grantee_idx
|
|
ON knoe.cross_grants (grantee_tenant, object_kind);
|
|
|
|
CREATE INDEX IF NOT EXISTS cross_grants_object_idx
|
|
ON knoe.cross_grants (object_uuid);
|
|
|
|
GRANT SELECT, INSERT, DELETE ON TABLE knoe.cross_grants TO knoe;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.grant_object(object_uuid, kind, grantee_tenant, privileges, expires_at)
|
|
--
|
|
-- Record that the owning tenant shares an object with another ekosystem.
|
|
-- Validates that the caller's tenant actually owns the object (via UUID bits).
|
|
-- On conflict: refreshes privileges and expiry (upsert semantics).
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION knoe.grant_object(
|
|
p_object_uuid text,
|
|
p_kind text,
|
|
p_grantee_tenant smallint,
|
|
p_privileges text[] DEFAULT '{SELECT}',
|
|
p_expires_at timestamptz DEFAULT NULL
|
|
) RETURNS knoe.cross_grants
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_grantor smallint;
|
|
v_row knoe.cross_grants;
|
|
BEGIN
|
|
-- Derive owner from UUID bits — no registry join needed
|
|
SELECT tenant_id INTO v_grantor FROM knoe.ekosystem_decode(p_object_uuid);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM knoe.tenants WHERE tenant_id = p_grantee_tenant) THEN
|
|
RAISE EXCEPTION 'Grantee tenant % is not a registered ekosystem', p_grantee_tenant;
|
|
END IF;
|
|
|
|
IF v_grantor = p_grantee_tenant THEN
|
|
RAISE EXCEPTION
|
|
'Grant target is the object owner (tenant %). Use standard PG GRANT within an ekosystem.',
|
|
v_grantor;
|
|
END IF;
|
|
|
|
INSERT INTO knoe.cross_grants
|
|
(object_uuid, object_kind, grantor_tenant, grantee_tenant, privileges, expires_at)
|
|
VALUES
|
|
(p_object_uuid, p_kind, v_grantor, p_grantee_tenant, p_privileges, p_expires_at)
|
|
ON CONFLICT (object_uuid, object_kind, grantee_tenant) DO UPDATE
|
|
SET privileges = EXCLUDED.privileges,
|
|
expires_at = EXCLUDED.expires_at,
|
|
granted_at = now()
|
|
RETURNING * INTO v_row;
|
|
|
|
RETURN v_row;
|
|
END;
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.revoke_object(object_uuid, kind, grantee_tenant)
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION knoe.revoke_object(
|
|
p_object_uuid text,
|
|
p_kind text,
|
|
p_grantee_tenant smallint
|
|
) RETURNS boolean
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
DELETE FROM knoe.cross_grants
|
|
WHERE object_uuid = p_object_uuid
|
|
AND object_kind = p_kind
|
|
AND grantee_tenant = p_grantee_tenant;
|
|
RETURN FOUND;
|
|
END;
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.can_access(object_uuid, kind, tenant_id) → boolean
|
|
--
|
|
-- True when:
|
|
-- (a) the tenant owns the object (tenant_id embedded in UUID), OR
|
|
-- (b) a non-expired cross-grant exists, OR
|
|
-- (c) tenant 0 (knoe-db root) — root has universal read access
|
|
--
|
|
-- STABLE: safe in WHERE clauses and on replicas.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION knoe.can_access(
|
|
p_object_uuid text,
|
|
p_kind text,
|
|
p_tenant_id smallint
|
|
) RETURNS boolean
|
|
LANGUAGE sql STABLE
|
|
AS $$
|
|
SELECT
|
|
p_tenant_id = 0 -- root authority
|
|
OR
|
|
(SELECT tenant_id
|
|
FROM knoe.ekosystem_decode(p_object_uuid)) = p_tenant_id -- owner
|
|
OR
|
|
EXISTS (
|
|
SELECT 1 FROM knoe.cross_grants
|
|
WHERE object_uuid = p_object_uuid
|
|
AND object_kind = p_kind
|
|
AND grantee_tenant = p_tenant_id
|
|
AND (expires_at IS NULL OR expires_at > now())
|
|
);
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- knoe.my_grants(tenant_id) → active cross-grants visible to this ekosystem
|
|
-- Includes both grants received and grants issued by this tenant.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION knoe.my_grants(p_tenant_id smallint)
|
|
RETURNS TABLE(
|
|
direction text, -- 'received' | 'issued'
|
|
object_uuid text,
|
|
object_kind text,
|
|
object_name text, -- prefixed identifier
|
|
other_tenant smallint,
|
|
privileges text[],
|
|
granted_at timestamptz,
|
|
expires_at timestamptz
|
|
)
|
|
LANGUAGE sql STABLE
|
|
AS $$
|
|
SELECT 'received'::text,
|
|
g.object_uuid,
|
|
g.object_kind,
|
|
knoe.ekosystem_object(g.object_uuid, g.object_kind),
|
|
g.grantor_tenant,
|
|
g.privileges,
|
|
g.granted_at,
|
|
g.expires_at
|
|
FROM knoe.cross_grants g
|
|
WHERE g.grantee_tenant = p_tenant_id
|
|
AND (g.expires_at IS NULL OR g.expires_at > now())
|
|
|
|
UNION ALL
|
|
|
|
SELECT 'issued'::text,
|
|
g.object_uuid,
|
|
g.object_kind,
|
|
knoe.ekosystem_object(g.object_uuid, g.object_kind),
|
|
g.grantee_tenant,
|
|
g.privileges,
|
|
g.granted_at,
|
|
g.expires_at
|
|
FROM knoe.cross_grants g
|
|
WHERE g.grantor_tenant = p_tenant_id
|
|
AND (g.expires_at IS NULL OR g.expires_at > now());
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Grants (mirrors existing knoe schema pattern)
|
|
-- ---------------------------------------------------------------------------
|
|
GRANT EXECUTE ON FUNCTION knoe.ekosystem_object(text, text) TO knoe;
|
|
GRANT EXECUTE ON FUNCTION knoe.ekosystem_kind(text) TO knoe;
|
|
GRANT EXECUTE ON FUNCTION knoe.grant_object(text, text, smallint, text[], timestamptz)
|
|
TO knoe;
|
|
GRANT EXECUTE ON FUNCTION knoe.revoke_object(text, text, smallint) TO knoe;
|
|
GRANT EXECUTE ON FUNCTION knoe.can_access(text, text, smallint) TO knoe;
|
|
GRANT EXECUTE ON FUNCTION knoe.my_grants(smallint) TO knoe;
|