Multi-Tenant Queries
On this page
- Default: Strict Tenant Isolation
- When You Need Cross-Tenant Access
- Data Sharing API
- Grant Patterns
- Pattern 1: One-Time Access
- Pattern 2: Standing Access
- Pattern 3: Conditional Access
- Querying Shared Data
- From Rust
- From Python
- Audit Trail
- Revoking Access
- Parent-Child Tenants
- Data Sharing Agreements
- Query Aggregates Across Tenants
- Best Practices
- 1. Always Require Justification
- 2. Use Shortest Possible Expiration
- 3. Grant Minimal Scope
- 4. Review Grants Regularly
- 5. Test Revocation
- Limitations
- No Direct SQL Joins
- No Wildcard Grants
- Related Documentation
Safely query data across tenants in Kimberlite.
Default: Strict Tenant Isolation
By default, tenants cannot query each other’s data:
-- Client authenticated as Tenant 1
SELECT * FROM patients WHERE id = 123;
-- Returns: Tenant 1's patient 123 (if exists)
-- Client authenticated as Tenant 2
SELECT * FROM patients WHERE id = 123;
-- Returns: Tenant 2's patient 123 (if exists)
-- CANNOT see Tenant 1's data
This is enforced at the protocol level—no way to bypass it with SQL.
When You Need Cross-Tenant Access
Sometimes cross-tenant queries are legitimate:
| Use Case | Example |
|---|---|
| Multi-hospital referrals | Hospital A sends patient to Hospital B |
| Shared research data | Multiple institutions collaborate on study |
| Parent organization | Corporate entity oversees subsidiaries |
| Data sharing agreements | Explicit consent to share specific records |
Data Sharing API
Use the explicit data sharing API (not SQL):
use Client;
// Tenant A grants read access to Tenant B
client.grant_access?;
// Now Tenant B can query that stream
let data = client_b.query_shared_stream?;
Key properties:
- Explicit: Requires grant, not implicit JOIN
- Audited: All cross-tenant access logged
- Revocable: Grants can be revoked at any time
- Time-limited: Grants expire automatically
- Fine-grained: Grant access to specific streams, not entire tenant
Grant Patterns
Pattern 1: One-Time Access
// Grant access for 1 hour
client.grant_access?;
// Tenant B reads data
let data = client_b.query_shared_stream?;
// Grant automatically expires after 1 hour
Pattern 2: Standing Access
// Grant long-term access (1 year)
client.grant_access?;
Pattern 3: Conditional Access
// Grant access only if patient consented
if patient_consented_to_sharing? else
Querying Shared Data
From Rust
use Client;
// Tenant B queries Tenant A's shared data
let shared_data = client_b.query_shared?;
// Check if we have access
match shared_data
From Python
=
# Query Tenant A's shared data
=
Audit Trail
All cross-tenant access is logged:
-- Query cross-tenant access log
SELECT
from_tenant,
to_tenant,
stream,
accessed_at,
accessed_by
FROM __cross_tenant_access_log
WHERE from_tenant = 1
AND accessed_at > NOW - INTERVAL '30 days'
ORDER BY accessed_at DESC;
Logged information:
- Which tenant accessed data
- Which tenant’s data was accessed
- What stream was accessed
- When the access occurred
- Which user performed the access
- Whether access was granted or denied
Revoking Access
// Revoke Tenant B's access to Tenant A's data
client_a.revoke_access?;
// Tenant B's subsequent queries will fail
let result = client_b.query_shared_stream?;
// Error: AccessDenied
Parent-Child Tenants
For parent organizations that need to see all child data:
// Create parent-child relationship
client.create_tenant_hierarchy?;
// Parent automatically has read access to all children
let all_patients = client_parent.query_descendants?;
Use case: Corporate entity needs to generate reports across all subsidiaries.
Data Sharing Agreements
Formalize sharing with contracts:
// Create agreement
let agreement = client.create_data_sharing_agreement?;
// Grant access based on agreement
client.grant_access_with_agreement?;
Query Aggregates Across Tenants
For analytics across tenants (no PHI):
// Get aggregate stats (no individual records)
let stats = client_admin.query_aggregate?;
// Result: Aggregate statistics only, no individual records
// { count: 5000, avg_age: 42.5 }
Use case: Generate reports without exposing individual patient data.
Best Practices
1. Always Require Justification
// Good
grant_access?;
// Bad: No justification
grant_access?;
2. Use Shortest Possible Expiration
// Good: 1 hour for one-time access
expiration: Some
// Bad: Indefinite access
expiration: None
3. Grant Minimal Scope
// Good: Specific stream (single patient)
stream: new
// Bad: All streams (entire tenant)
stream: wildcard // DON'T DO THIS
4. Review Grants Regularly
-- Find grants expiring soon
SELECT * FROM __data_sharing_grants
WHERE expiration < NOW + INTERVAL '7 days';
-- Find unused grants
SELECT * FROM __data_sharing_grants g
LEFT JOIN __cross_tenant_access_log a ON g.grant_id = a.grant_id
WHERE a.grant_id IS NULL;
5. Test Revocation
Limitations
No Direct SQL Joins
-- This is NOT possible
SELECT t1.name, t2.appointments
FROM tenant_1.patients t1
JOIN tenant_2.appointments t2 ON t1.id = t2.patient_id;
-- Error: Cross-tenant SQL joins not supported
Workaround: Use the data sharing API, not SQL.
No Wildcard Grants
// This is NOT possible
grant_access?;
// Error: Must specify exact stream
Why: Too broad, violates principle of least privilege.
Related Documentation
- Multi-tenancy - Tenant isolation architecture
- Data Sharing Design - Implementation details
- Compliance - Audit requirements
Key Takeaway: Kimberlite enforces strict tenant isolation by default. Cross-tenant access requires explicit grants through the data sharing API, not SQL. All cross-tenant access is audited.