feat(server): v2 secure-session-core Task 1 - schema + per-agent keys
All checks were successful
All checks were successful
SPEC-002 Phase 1 Task 1 (specs/v2-secure-session-core), code-reviewed APPROVED. Migration 004 (idempotent, server-applied): tenants + seeded default tenant, connect_agent_keys (hash-only, revocable, FK->connect_machines), nullable tenant_id on all scoped tables (tenancy-ready, not tenant-yet), connect_sessions is_managed/source/consent_state, connect_support_codes consumed_at. New db modules agent_keys.rs (stores only key_hash) + tenancy.rs (DEFAULT_TENANT_ID, Phase-4 switch point). Struct/query updates across machines/sessions/ support_codes/events/users. Runtime sqlx throughout (GC db layer already uses it - no compile-time macros). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
114
server/migrations/004_v2_secure_session_core.sql
Normal file
114
server/migrations/004_v2_secure_session_core.sql
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
-- Migration: 004_v2_secure_session_core.sql
|
||||||
|
-- Purpose: v2 Secure Session Core (Task 1) - per-agent keys + tenancy-ready schema.
|
||||||
|
-- Adds the tenants table + a fixed default tenant, the connect_agent_keys
|
||||||
|
-- table, a nullable tenant_id on every scoped table (backfilled to the
|
||||||
|
-- default tenant), and the new session/support-code state columns.
|
||||||
|
--
|
||||||
|
-- Idempotent: CREATE TABLE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS / ON CONFLICT.
|
||||||
|
-- Applied on server startup by sqlx::migrate!(); never pre-applied via psql.
|
||||||
|
-- See .claude/standards/gururmm/sqlx-migrations.md.
|
||||||
|
|
||||||
|
-- pgcrypto provides gen_random_uuid(); enabled in 001 but re-asserted for safety.
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Tenants (tenancy-ready; isolation NOT enforced until Phase 4)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tenants (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Seed exactly one default tenant with a FIXED, hard-coded UUID. This constant
|
||||||
|
-- is the Phase-4 multi-tenancy switch point (mirrored by
|
||||||
|
-- db::tenancy::DEFAULT_TENANT_ID). Idempotent via ON CONFLICT.
|
||||||
|
INSERT INTO tenants (id, name)
|
||||||
|
VALUES ('00000000-0000-0000-0000-000000000001', 'Default Tenant')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Per-agent keys (connect_agent_keys) - replaces the shared AGENT_API_KEY
|
||||||
|
-- ============================================================================
|
||||||
|
-- Stores ONLY the key hash. Hashing + cak_ issuance is Task 2; this table
|
||||||
|
-- persists an already-hashed value. revoked_at set marks a key inactive.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS connect_agent_keys (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
machine_id UUID NOT NULL REFERENCES connect_machines(id) ON DELETE CASCADE,
|
||||||
|
key_hash TEXT NOT NULL UNIQUE,
|
||||||
|
tenant_id UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_used_at TIMESTAMPTZ,
|
||||||
|
revoked_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_connect_agent_keys_machine ON connect_agent_keys(machine_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_connect_agent_keys_key_hash ON connect_agent_keys(key_hash);
|
||||||
|
|
||||||
|
-- Backfill the agent-keys tenant_id to the default tenant (table is empty on a
|
||||||
|
-- fresh DB; this is a no-op there but keeps the migration self-consistent).
|
||||||
|
UPDATE connect_agent_keys
|
||||||
|
SET tenant_id = '00000000-0000-0000-0000-000000000001'
|
||||||
|
WHERE tenant_id IS NULL;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- tenant_id on all scoped tables (nullable; backfilled to the default tenant)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE connect_machines ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||||
|
ALTER TABLE connect_sessions ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||||
|
ALTER TABLE connect_support_codes ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||||
|
ALTER TABLE connect_session_events ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id UUID;
|
||||||
|
|
||||||
|
UPDATE connect_machines
|
||||||
|
SET tenant_id = '00000000-0000-0000-0000-000000000001'
|
||||||
|
WHERE tenant_id IS NULL;
|
||||||
|
|
||||||
|
UPDATE connect_sessions
|
||||||
|
SET tenant_id = '00000000-0000-0000-0000-000000000001'
|
||||||
|
WHERE tenant_id IS NULL;
|
||||||
|
|
||||||
|
UPDATE connect_support_codes
|
||||||
|
SET tenant_id = '00000000-0000-0000-0000-000000000001'
|
||||||
|
WHERE tenant_id IS NULL;
|
||||||
|
|
||||||
|
UPDATE connect_session_events
|
||||||
|
SET tenant_id = '00000000-0000-0000-0000-000000000001'
|
||||||
|
WHERE tenant_id IS NULL;
|
||||||
|
|
||||||
|
UPDATE users
|
||||||
|
SET tenant_id = '00000000-0000-0000-0000-000000000001'
|
||||||
|
WHERE tenant_id IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_connect_machines_tenant ON connect_machines(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_connect_sessions_tenant ON connect_sessions(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_connect_support_codes_tenant ON connect_support_codes(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_connect_session_events_tenant ON connect_session_events(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Session state columns (managed/unattended + source + consent)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE connect_sessions
|
||||||
|
ADD COLUMN IF NOT EXISTS is_managed BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- source: 'standalone' (ad-hoc support code) | 'gururmm' (managed via RMM).
|
||||||
|
ALTER TABLE connect_sessions
|
||||||
|
ADD COLUMN IF NOT EXISTS source TEXT NOT NULL DEFAULT 'standalone'
|
||||||
|
CHECK (source IN ('standalone', 'gururmm'));
|
||||||
|
|
||||||
|
-- consent_state: not_required (managed/unattended) | pending | granted | denied.
|
||||||
|
-- Attended-consent enforcement is Task 5; this is the schema column only.
|
||||||
|
ALTER TABLE connect_sessions
|
||||||
|
ADD COLUMN IF NOT EXISTS consent_state TEXT NOT NULL DEFAULT 'not_required'
|
||||||
|
CHECK (consent_state IN ('not_required', 'pending', 'granted', 'denied'));
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Support-code single-use marker (consumed in Task 4 - schema only here)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE connect_support_codes ADD COLUMN IF NOT EXISTS consumed_at TIMESTAMPTZ;
|
||||||
105
server/src/db/agent_keys.rs
Normal file
105
server/src/db/agent_keys.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
//! Per-agent key database operations.
|
||||||
|
//!
|
||||||
|
//! Backs the `connect_agent_keys` table introduced in v2 (migration 004). A
|
||||||
|
//! per-agent key replaces the shared `AGENT_API_KEY`: each managed machine has
|
||||||
|
//! one or more revocable keys, stored ONLY as a hash.
|
||||||
|
//!
|
||||||
|
//! This module is hash-agnostic persistence. Computing the hash and minting the
|
||||||
|
//! plaintext `cak_` token is Task 2 - every function here takes an
|
||||||
|
//! already-hashed value. All queries use runtime `sqlx::query()` /
|
||||||
|
//! `sqlx::query_as()` per the v2 convention (no compile-time macros).
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Per-agent key record from the database.
|
||||||
|
///
|
||||||
|
/// `key_hash` is the only representation of the key the server ever stores; the
|
||||||
|
/// plaintext is shown once at issuance (Task 2) and never persisted.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
|
#[allow(dead_code)] // Consumed by agent auth + key issuance endpoints in Task 2.
|
||||||
|
pub struct AgentKey {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub machine_id: Uuid,
|
||||||
|
pub key_hash: String,
|
||||||
|
pub tenant_id: Option<Uuid>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub last_used_at: Option<DateTime<Utc>>,
|
||||||
|
pub revoked_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new per-agent key (already hashed) and return its id.
|
||||||
|
///
|
||||||
|
/// `tenant_id` is `None`-tolerant; pass `db::tenancy::current_tenant_id()` at
|
||||||
|
/// the call site once issuance lands in Task 2.
|
||||||
|
#[allow(dead_code)] // Wired by the key-issuance endpoint in Task 2.
|
||||||
|
pub async fn insert_agent_key(
|
||||||
|
pool: &PgPool,
|
||||||
|
machine_id: Uuid,
|
||||||
|
key_hash: &str,
|
||||||
|
tenant_id: Option<Uuid>,
|
||||||
|
) -> Result<Uuid, sqlx::Error> {
|
||||||
|
let id = sqlx::query_scalar::<_, Uuid>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO connect_agent_keys (machine_id, key_hash, tenant_id)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(machine_id)
|
||||||
|
.bind(key_hash)
|
||||||
|
.bind(tenant_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find an active (non-revoked) key by its hash.
|
||||||
|
///
|
||||||
|
/// Returns `None` if no row matches or the matching key has been revoked. Agent
|
||||||
|
/// authentication (Task 2) hashes the presented `cak_` token and looks it up
|
||||||
|
/// here, rejecting on `None`.
|
||||||
|
#[allow(dead_code)] // Wired by agent WS auth in Task 2/3.
|
||||||
|
pub async fn find_active_by_hash(
|
||||||
|
pool: &PgPool,
|
||||||
|
key_hash: &str,
|
||||||
|
) -> Result<Option<AgentKey>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, AgentKey>(
|
||||||
|
r#"
|
||||||
|
SELECT id, machine_id, key_hash, tenant_id, created_at, last_used_at, revoked_at
|
||||||
|
FROM connect_agent_keys
|
||||||
|
WHERE key_hash = $1 AND revoked_at IS NULL
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(key_hash)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Revoke a key by id (idempotent: re-revoking keeps the original timestamp).
|
||||||
|
#[allow(dead_code)] // Wired by the key-revocation endpoint in Task 2.
|
||||||
|
pub async fn revoke_agent_key(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE connect_agent_keys
|
||||||
|
SET revoked_at = NOW()
|
||||||
|
WHERE id = $1 AND revoked_at IS NULL
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stamp `last_used_at` on a successful authentication.
|
||||||
|
#[allow(dead_code)] // Wired by agent WS auth in Task 2/3.
|
||||||
|
pub async fn touch_last_used(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query("UPDATE connect_agent_keys SET last_used_at = NOW() WHERE id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ pub struct SessionEvent {
|
|||||||
pub viewer_name: Option<String>,
|
pub viewer_name: Option<String>,
|
||||||
pub details: Option<JsonValue>,
|
pub details: Option<JsonValue>,
|
||||||
pub ip_address: Option<String>,
|
pub ip_address: Option<String>,
|
||||||
|
/// Tenancy-ready (Phase 4). Backfilled to the default tenant by migration 004.
|
||||||
|
pub tenant_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event types for session audit logging
|
/// Event types for session audit logging
|
||||||
@@ -84,7 +86,7 @@ pub async fn get_session_events(
|
|||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
||||||
sqlx::query_as::<_, SessionEvent>(
|
sqlx::query_as::<_, SessionEvent>(
|
||||||
"SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address FROM connect_session_events WHERE session_id = $1 ORDER BY timestamp"
|
"SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address, tenant_id FROM connect_session_events WHERE session_id = $1 ORDER BY timestamp"
|
||||||
)
|
)
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
@@ -98,7 +100,7 @@ pub async fn get_recent_events(
|
|||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
||||||
sqlx::query_as::<_, SessionEvent>(
|
sqlx::query_as::<_, SessionEvent>(
|
||||||
"SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address FROM connect_session_events ORDER BY timestamp DESC LIMIT $1"
|
"SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address, tenant_id FROM connect_session_events ORDER BY timestamp DESC LIMIT $1"
|
||||||
)
|
)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
@@ -113,7 +115,7 @@ pub async fn get_events_by_type(
|
|||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
||||||
sqlx::query_as::<_, SessionEvent>(
|
sqlx::query_as::<_, SessionEvent>(
|
||||||
"SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address FROM connect_session_events WHERE event_type = $1 ORDER BY timestamp DESC LIMIT $2"
|
"SELECT id, session_id, event_type, timestamp, viewer_id, viewer_name, details, ip_address::text as ip_address, tenant_id FROM connect_session_events WHERE event_type = $1 ORDER BY timestamp DESC LIMIT $2"
|
||||||
)
|
)
|
||||||
.bind(event_type)
|
.bind(event_type)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
@@ -128,7 +130,7 @@ pub async fn get_events_for_machine(
|
|||||||
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
||||||
sqlx::query_as::<_, SessionEvent>(
|
sqlx::query_as::<_, SessionEvent>(
|
||||||
r#"
|
r#"
|
||||||
SELECT e.id, e.session_id, e.event_type, e.timestamp, e.viewer_id, e.viewer_name, e.details, e.ip_address::text as ip_address
|
SELECT e.id, e.session_id, e.event_type, e.timestamp, e.viewer_id, e.viewer_name, e.details, e.ip_address::text as ip_address, e.tenant_id
|
||||||
FROM connect_session_events e
|
FROM connect_session_events e
|
||||||
JOIN connect_sessions s ON e.session_id = s.id
|
JOIN connect_sessions s ON e.session_id = s.id
|
||||||
WHERE s.machine_id = $1
|
WHERE s.machine_id = $1
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub struct Machine {
|
|||||||
pub status: String,
|
pub status: String,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
|
/// Tenancy-ready (Phase 4). Backfilled to the default tenant by migration 004.
|
||||||
|
pub tenant_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get or create a machine by agent_id (upsert)
|
/// Get or create a machine by agent_id (upsert)
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
//! Handles persistence for machines, sessions, and audit logging.
|
//! Handles persistence for machines, sessions, and audit logging.
|
||||||
//! Optional - server works without database if DATABASE_URL not set.
|
//! Optional - server works without database if DATABASE_URL not set.
|
||||||
|
|
||||||
|
pub mod agent_keys;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod machines;
|
pub mod machines;
|
||||||
pub mod releases;
|
pub mod releases;
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
pub mod support_codes;
|
pub mod support_codes;
|
||||||
|
pub mod tenancy;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ pub struct DbSession {
|
|||||||
pub is_support_session: bool,
|
pub is_support_session: bool,
|
||||||
pub support_code: Option<String>,
|
pub support_code: Option<String>,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
/// Tenancy-ready (Phase 4). Backfilled to the default tenant by migration 004.
|
||||||
|
pub tenant_id: Option<Uuid>,
|
||||||
|
/// True for managed/unattended (RMM) sessions; false for ad-hoc support.
|
||||||
|
pub is_managed: bool,
|
||||||
|
/// Origin of the session: 'standalone' (support code) | 'gururmm' (managed).
|
||||||
|
pub source: String,
|
||||||
|
/// Attended-consent state: 'not_required' | 'pending' | 'granted' | 'denied'.
|
||||||
|
/// Enforcement is Task 5; this column carries the state only.
|
||||||
|
pub consent_state: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new session record
|
/// Create a new session record
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ pub struct DbSupportCode {
|
|||||||
pub client_name: Option<String>,
|
pub client_name: Option<String>,
|
||||||
pub client_machine: Option<String>,
|
pub client_machine: Option<String>,
|
||||||
pub connected_at: Option<DateTime<Utc>>,
|
pub connected_at: Option<DateTime<Utc>>,
|
||||||
|
/// Single-use marker. Set when a code is consumed on first agent bind.
|
||||||
|
/// Consume logic is Task 4; this column carries the marker only.
|
||||||
|
pub consumed_at: Option<DateTime<Utc>>,
|
||||||
|
/// Tenancy-ready (Phase 4). Backfilled to the default tenant by migration 004.
|
||||||
|
pub tenant_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new support code
|
/// Create a new support code
|
||||||
|
|||||||
31
server/src/db/tenancy.rs
Normal file
31
server/src/db/tenancy.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
//! Tenancy resolution.
|
||||||
|
//!
|
||||||
|
//! GuruConnect v2 ships a tenancy-*ready* schema: every scoped table carries a
|
||||||
|
//! nullable `tenant_id`, and a single fixed default tenant exists. Isolation is
|
||||||
|
//! NOT enforced in Phase 1 - every call resolves to the default tenant here.
|
||||||
|
//!
|
||||||
|
//! Phase 4 (multi-tenancy activation) flips this module to resolve the tenant
|
||||||
|
//! from the authenticated principal (viewer JWT claim / per-agent key row)
|
||||||
|
//! instead of returning the constant. Keeping the resolution behind a single
|
||||||
|
//! function means no schema rewrite and no scattered call-site changes.
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// The fixed default tenant UUID.
|
||||||
|
///
|
||||||
|
/// Must stay in lockstep with the seed row in
|
||||||
|
/// `migrations/004_v2_secure_session_core.sql`
|
||||||
|
/// (`00000000-0000-0000-0000-000000000001`).
|
||||||
|
pub const DEFAULT_TENANT_ID: Uuid = Uuid::from_u128(0x0000_0000_0000_0000_0000_0000_0000_0001);
|
||||||
|
|
||||||
|
/// Resolve the tenant for the current operation.
|
||||||
|
///
|
||||||
|
/// PHASE-4 SWITCH POINT: today this unconditionally returns
|
||||||
|
/// [`DEFAULT_TENANT_ID`]. When multi-tenancy is activated, replace the body
|
||||||
|
/// with resolution from the authenticated principal; the signature should grow
|
||||||
|
/// the principal/context argument at that time. All scoped inserts route their
|
||||||
|
/// `tenant_id` through this function so the cutover is localized here.
|
||||||
|
#[allow(dead_code)] // Wired into inserts as call sites adopt tenancy (Tasks 2-4); single Phase-4 switch point.
|
||||||
|
pub fn current_tenant_id() -> Uuid {
|
||||||
|
DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
@@ -19,6 +19,9 @@ pub struct User {
|
|||||||
// TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
|
// TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub last_login: Option<DateTime<Utc>>,
|
pub last_login: Option<DateTime<Utc>>,
|
||||||
|
/// Tenancy-ready (Phase 4). Backfilled to the default tenant by migration 004.
|
||||||
|
#[allow(dead_code)] // Surfaced by tenant-aware auth in Phase 4; column exists now.
|
||||||
|
pub tenant_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User without password hash (for API responses)
|
/// User without password hash (for API responses)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# v2 Secure Session Core — Implementation Plan
|
# v2 Secure Session Core — Implementation Plan
|
||||||
|
|
||||||
> Spec created: 2026-05-29
|
> Spec created: 2026-05-29
|
||||||
> Status: not started
|
> Status: in progress — Task 1 (schema) DONE 2026-05-29; Task 2 (auth) next
|
||||||
> Parent: `docs/specs/SPEC-002-v2-modernization-architecture.md` (Phase 1)
|
> Parent: `docs/specs/SPEC-002-v2-modernization-architecture.md` (Phase 1)
|
||||||
> Keystone: Tasks 1–4 are the "get-right-first" secure auth/session core — every audit CRITICAL/HIGH
|
> Keystone: Tasks 1–4 are the "get-right-first" secure auth/session core — every audit CRITICAL/HIGH
|
||||||
> is closed there. Tasks 5–7 deliver the product capability on top. Do them in order.
|
> is closed there. Tasks 5–7 deliver the product capability on top. Do them in order.
|
||||||
@@ -19,7 +19,11 @@ Do not start Task 1 until this commit exists.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 1 (KEYSTONE): v2 schema — per-agent keys + tenancy-ready tables
|
## Task 1 (KEYSTONE) [DONE 2026-05-29]: v2 schema — per-agent keys + tenancy-ready tables
|
||||||
|
|
||||||
|
> [DONE] migration `004_v2_secure_session_core.sql` + `db/agent_keys.rs` + `db/tenancy.rs` + struct/query
|
||||||
|
> updates across machines/sessions/support_codes/events/users. Code-reviewed APPROVED. Note: GC's db
|
||||||
|
> layer already uses runtime `sqlx::query()` (no macros) — the v2 "switch to runtime" was already true.
|
||||||
|
|
||||||
Files touched: `server/migrations/` (new v2 migration files), `server/src/db/` (rebuilt modules:
|
Files touched: `server/migrations/` (new v2 migration files), `server/src/db/` (rebuilt modules:
|
||||||
`agent_keys.rs` [new], `sessions.rs`, `machines.rs`, `support_codes.rs`, `events.rs`, `users.rs`,
|
`agent_keys.rs` [new], `sessions.rs`, `machines.rs`, `support_codes.rs`, `events.rs`, `users.rs`,
|
||||||
|
|||||||
Reference in New Issue
Block a user