kailash 0.6.1__py3-none-any.whl → 0.6.2__py3-none-any.whl

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.
@@ -0,0 +1,387 @@
1
+ -- Unified Admin Node Database Schema
2
+ -- Complete RBAC/ABAC system with user management and permission checking
3
+ -- Production-ready schema for Kailash Admin Nodes
4
+
5
+ -- =====================================================
6
+ -- Core User Management
7
+ -- =====================================================
8
+
9
+ -- Users table - Central user registry
10
+ CREATE TABLE IF NOT EXISTS users (
11
+ user_id VARCHAR(255) PRIMARY KEY,
12
+ email VARCHAR(255) UNIQUE NOT NULL,
13
+ username VARCHAR(255),
14
+ password_hash VARCHAR(255), -- Optional, for local auth
15
+ first_name VARCHAR(255),
16
+ last_name VARCHAR(255),
17
+ display_name VARCHAR(255),
18
+
19
+ -- Admin node compatibility fields
20
+ roles JSONB DEFAULT '[]', -- For compatibility with PermissionCheckNode
21
+ attributes JSONB DEFAULT '{}', -- User attributes for ABAC
22
+
23
+ -- Status and lifecycle
24
+ status VARCHAR(50) DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended', 'pending', 'deleted')),
25
+ is_active BOOLEAN DEFAULT TRUE,
26
+ is_system_user BOOLEAN DEFAULT FALSE,
27
+
28
+ -- Multi-tenancy
29
+ tenant_id VARCHAR(255) NOT NULL,
30
+
31
+ -- External auth integration
32
+ external_auth_id VARCHAR(255), -- For SSO integration
33
+ auth_provider VARCHAR(100), -- 'local', 'oauth2', 'saml', etc.
34
+
35
+ -- Audit fields
36
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
37
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
38
+ created_by VARCHAR(255) DEFAULT 'system',
39
+ last_login_at TIMESTAMP WITH TIME ZONE,
40
+
41
+ -- Constraints
42
+ UNIQUE(email, tenant_id),
43
+ UNIQUE(username, tenant_id) -- Allow same username across tenants
44
+ );
45
+
46
+ -- =====================================================
47
+ -- Role-Based Access Control (RBAC)
48
+ -- =====================================================
49
+
50
+ -- Roles table - Hierarchical role definitions
51
+ CREATE TABLE IF NOT EXISTS roles (
52
+ role_id VARCHAR(255) PRIMARY KEY,
53
+ name VARCHAR(255) NOT NULL,
54
+ description TEXT,
55
+ role_type VARCHAR(50) DEFAULT 'custom' CHECK (role_type IN ('system', 'custom', 'template', 'temporary')),
56
+
57
+ -- RBAC permissions and hierarchy
58
+ permissions JSONB DEFAULT '[]', -- Direct permissions
59
+ parent_roles JSONB DEFAULT '[]', -- Role inheritance
60
+ child_roles JSONB DEFAULT '[]', -- Derived roles (maintained automatically)
61
+
62
+ -- ABAC attributes and conditions
63
+ attributes JSONB DEFAULT '{}', -- Role attributes
64
+ conditions JSONB DEFAULT '{}', -- Dynamic conditions for role activation
65
+
66
+ -- Lifecycle and constraints
67
+ is_active BOOLEAN DEFAULT TRUE,
68
+ is_system_role BOOLEAN DEFAULT FALSE,
69
+ expires_at TIMESTAMP WITH TIME ZONE, -- For temporary roles
70
+
71
+ -- Multi-tenancy
72
+ tenant_id VARCHAR(255) NOT NULL,
73
+
74
+ -- Audit fields
75
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
76
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
77
+ created_by VARCHAR(255) DEFAULT 'system',
78
+
79
+ -- Constraints
80
+ UNIQUE(name, tenant_id)
81
+ );
82
+
83
+ -- User Role Assignments - Many-to-many with metadata
84
+ CREATE TABLE IF NOT EXISTS user_role_assignments (
85
+ id SERIAL PRIMARY KEY,
86
+ user_id VARCHAR(255) NOT NULL,
87
+ role_id VARCHAR(255) NOT NULL,
88
+ tenant_id VARCHAR(255) NOT NULL,
89
+
90
+ -- Assignment metadata
91
+ assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
92
+ assigned_by VARCHAR(255) DEFAULT 'system',
93
+ expires_at TIMESTAMP WITH TIME ZONE, -- For temporary assignments
94
+
95
+ -- Conditional assignments (ABAC)
96
+ conditions JSONB DEFAULT '{}', -- When this assignment is active
97
+ context_requirements JSONB DEFAULT '{}', -- Required context for activation
98
+
99
+ -- Status
100
+ is_active BOOLEAN DEFAULT TRUE,
101
+ is_inherited BOOLEAN DEFAULT FALSE, -- True if inherited from group/org
102
+
103
+ -- Constraints
104
+ UNIQUE(user_id, role_id, tenant_id),
105
+
106
+ -- Foreign keys (enforced at application level for flexibility)
107
+ CONSTRAINT fk_user_role_user CHECK (user_id IS NOT NULL),
108
+ CONSTRAINT fk_user_role_role CHECK (role_id IS NOT NULL)
109
+ );
110
+
111
+ -- =====================================================
112
+ -- Permission Management and Caching
113
+ -- =====================================================
114
+
115
+ -- Permission definitions - Centralized permission registry
116
+ CREATE TABLE IF NOT EXISTS permissions (
117
+ permission_id VARCHAR(255) PRIMARY KEY,
118
+ name VARCHAR(255) NOT NULL,
119
+ description TEXT,
120
+ resource_type VARCHAR(100) NOT NULL, -- 'workflow', 'node', 'data', etc.
121
+ action VARCHAR(100) NOT NULL, -- 'read', 'write', 'execute', etc.
122
+
123
+ -- Permission metadata
124
+ scope VARCHAR(100) DEFAULT 'tenant', -- 'global', 'tenant', 'user'
125
+ is_system_permission BOOLEAN DEFAULT FALSE,
126
+
127
+ -- ABAC conditions
128
+ default_conditions JSONB DEFAULT '{}', -- Default conditions for this permission
129
+ required_attributes JSONB DEFAULT '{}', -- Required user/resource attributes
130
+
131
+ -- Multi-tenancy
132
+ tenant_id VARCHAR(255) NOT NULL,
133
+
134
+ -- Audit fields
135
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
136
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
137
+
138
+ -- Constraints
139
+ UNIQUE(name, resource_type, action, tenant_id)
140
+ );
141
+
142
+ -- Permission Cache - High-performance permission checking
143
+ CREATE TABLE IF NOT EXISTS permission_cache (
144
+ cache_key VARCHAR(512) PRIMARY KEY,
145
+ user_id VARCHAR(255) NOT NULL,
146
+ resource VARCHAR(255) NOT NULL,
147
+ permission VARCHAR(255) NOT NULL,
148
+ tenant_id VARCHAR(255) NOT NULL,
149
+
150
+ -- Cache result
151
+ result BOOLEAN NOT NULL,
152
+ decision_path JSONB, -- How the decision was made (for auditing)
153
+ context_hash VARCHAR(64), -- Hash of context used for decision
154
+
155
+ -- Cache metadata
156
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
157
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
158
+ hit_count INTEGER DEFAULT 0,
159
+ last_accessed TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
160
+
161
+ -- Indexes will be created separately
162
+ CHECK (expires_at > created_at)
163
+ );
164
+
165
+ -- =====================================================
166
+ -- Attribute-Based Access Control (ABAC)
167
+ -- =====================================================
168
+
169
+ -- User Attributes - Dynamic user properties for ABAC
170
+ CREATE TABLE IF NOT EXISTS user_attributes (
171
+ id SERIAL PRIMARY KEY,
172
+ user_id VARCHAR(255) NOT NULL,
173
+ tenant_id VARCHAR(255) NOT NULL,
174
+
175
+ -- Attribute definition
176
+ attribute_name VARCHAR(255) NOT NULL,
177
+ attribute_value JSONB NOT NULL,
178
+ attribute_type VARCHAR(50) DEFAULT 'string', -- 'string', 'number', 'boolean', 'array', 'object'
179
+
180
+ -- Attribute metadata
181
+ is_computed BOOLEAN DEFAULT FALSE, -- True if computed from other attributes
182
+ computation_rule JSONB, -- How to compute this attribute
183
+ source VARCHAR(100) DEFAULT 'manual', -- 'manual', 'computed', 'imported', 'inherited'
184
+
185
+ -- Lifecycle
186
+ is_active BOOLEAN DEFAULT TRUE,
187
+ expires_at TIMESTAMP WITH TIME ZONE,
188
+
189
+ -- Audit fields
190
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
191
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
192
+ created_by VARCHAR(255) DEFAULT 'system',
193
+
194
+ -- Constraints
195
+ UNIQUE(user_id, attribute_name, tenant_id)
196
+ );
197
+
198
+ -- Resource Attributes - Dynamic resource properties for ABAC
199
+ CREATE TABLE IF NOT EXISTS resource_attributes (
200
+ id SERIAL PRIMARY KEY,
201
+ resource_id VARCHAR(255) NOT NULL,
202
+ resource_type VARCHAR(100) NOT NULL, -- 'workflow', 'node', 'data', etc.
203
+ tenant_id VARCHAR(255) NOT NULL,
204
+
205
+ -- Attribute definition
206
+ attribute_name VARCHAR(255) NOT NULL,
207
+ attribute_value JSONB NOT NULL,
208
+ attribute_type VARCHAR(50) DEFAULT 'string',
209
+
210
+ -- Attribute metadata
211
+ is_computed BOOLEAN DEFAULT FALSE,
212
+ computation_rule JSONB,
213
+ source VARCHAR(100) DEFAULT 'manual',
214
+
215
+ -- Lifecycle
216
+ is_active BOOLEAN DEFAULT TRUE,
217
+ expires_at TIMESTAMP WITH TIME ZONE,
218
+
219
+ -- Audit fields
220
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
221
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
222
+ created_by VARCHAR(255) DEFAULT 'system',
223
+
224
+ -- Constraints
225
+ UNIQUE(resource_id, attribute_name, tenant_id)
226
+ );
227
+
228
+ -- =====================================================
229
+ -- Sessions and Security
230
+ -- =====================================================
231
+
232
+ -- User Sessions - Track active user sessions
233
+ CREATE TABLE IF NOT EXISTS user_sessions (
234
+ session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
235
+ user_id VARCHAR(255) NOT NULL,
236
+ tenant_id VARCHAR(255) NOT NULL,
237
+
238
+ -- Session data
239
+ session_token_hash VARCHAR(255) UNIQUE NOT NULL,
240
+ refresh_token_hash VARCHAR(255),
241
+ device_info JSONB DEFAULT '{}',
242
+ ip_address INET,
243
+ user_agent TEXT,
244
+
245
+ -- Session lifecycle
246
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
247
+ last_accessed TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
248
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
249
+ is_active BOOLEAN DEFAULT TRUE,
250
+
251
+ -- Security
252
+ failed_attempts INTEGER DEFAULT 0,
253
+ locked_until TIMESTAMP WITH TIME ZONE,
254
+
255
+ CHECK (expires_at > created_at)
256
+ );
257
+
258
+ -- =====================================================
259
+ -- Audit and Compliance
260
+ -- =====================================================
261
+
262
+ -- Admin Audit Log - Comprehensive audit trail
263
+ CREATE TABLE IF NOT EXISTS admin_audit_log (
264
+ id SERIAL PRIMARY KEY,
265
+
266
+ -- Who, What, When, Where
267
+ user_id VARCHAR(255),
268
+ tenant_id VARCHAR(255) NOT NULL,
269
+ action VARCHAR(100) NOT NULL,
270
+ resource_type VARCHAR(100) NOT NULL,
271
+ resource_id VARCHAR(255),
272
+
273
+ -- Action details
274
+ operation VARCHAR(100), -- 'create', 'read', 'update', 'delete', 'execute'
275
+ old_values JSONB, -- Before state
276
+ new_values JSONB, -- After state
277
+ context JSONB DEFAULT '{}', -- Request context
278
+
279
+ -- Result
280
+ success BOOLEAN NOT NULL,
281
+ error_message TEXT,
282
+ duration_ms INTEGER,
283
+
284
+ -- Request metadata
285
+ ip_address INET,
286
+ user_agent TEXT,
287
+ session_id UUID,
288
+ request_id VARCHAR(255),
289
+
290
+ -- Timestamp
291
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
292
+ );
293
+
294
+ -- =====================================================
295
+ -- Performance Indexes
296
+ -- =====================================================
297
+
298
+ -- User indexes
299
+ CREATE INDEX IF NOT EXISTS idx_users_tenant_status ON users(tenant_id, status);
300
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
301
+ CREATE INDEX IF NOT EXISTS idx_users_external_auth ON users(external_auth_id, auth_provider);
302
+ CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login_at);
303
+
304
+ -- Role indexes
305
+ CREATE INDEX IF NOT EXISTS idx_roles_tenant_active ON roles(tenant_id, is_active);
306
+ CREATE INDEX IF NOT EXISTS idx_roles_type ON roles(role_type);
307
+ CREATE INDEX IF NOT EXISTS idx_roles_parent_roles ON roles USING GIN(parent_roles);
308
+ CREATE INDEX IF NOT EXISTS idx_roles_permissions ON roles USING GIN(permissions);
309
+
310
+ -- Assignment indexes
311
+ CREATE INDEX IF NOT EXISTS idx_user_roles_user ON user_role_assignments(user_id, tenant_id);
312
+ CREATE INDEX IF NOT EXISTS idx_user_roles_role ON user_role_assignments(role_id, tenant_id);
313
+ CREATE INDEX IF NOT EXISTS idx_user_roles_active ON user_role_assignments(is_active, expires_at);
314
+
315
+ -- Permission cache indexes
316
+ CREATE INDEX IF NOT EXISTS idx_permission_cache_user ON permission_cache(user_id, tenant_id);
317
+ CREATE INDEX IF NOT EXISTS idx_permission_cache_expires ON permission_cache(expires_at);
318
+ CREATE INDEX IF NOT EXISTS idx_permission_cache_resource ON permission_cache(resource, permission);
319
+
320
+ -- Attribute indexes
321
+ CREATE INDEX IF NOT EXISTS idx_user_attributes_user ON user_attributes(user_id, tenant_id);
322
+ CREATE INDEX IF NOT EXISTS idx_user_attributes_name ON user_attributes(attribute_name, is_active);
323
+ CREATE INDEX IF NOT EXISTS idx_resource_attributes_resource ON resource_attributes(resource_id, resource_type);
324
+
325
+ -- Session indexes
326
+ CREATE INDEX IF NOT EXISTS idx_sessions_user ON user_sessions(user_id, tenant_id);
327
+ CREATE INDEX IF NOT EXISTS idx_sessions_token ON user_sessions(session_token_hash);
328
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires ON user_sessions(expires_at, is_active);
329
+
330
+ -- Audit indexes
331
+ CREATE INDEX IF NOT EXISTS idx_audit_user ON admin_audit_log(user_id, tenant_id);
332
+ CREATE INDEX IF NOT EXISTS idx_audit_resource ON admin_audit_log(resource_type, resource_id);
333
+ CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON admin_audit_log(created_at);
334
+ CREATE INDEX IF NOT EXISTS idx_audit_action ON admin_audit_log(action, operation);
335
+
336
+ -- =====================================================
337
+ -- Data Integrity and Maintenance
338
+ -- =====================================================
339
+
340
+ -- Auto-update timestamps trigger function
341
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
342
+ RETURNS TRIGGER AS $$
343
+ BEGIN
344
+ NEW.updated_at = CURRENT_TIMESTAMP;
345
+ RETURN NEW;
346
+ END;
347
+ $$ language 'plpgsql';
348
+
349
+ -- Apply auto-update triggers
350
+ CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
351
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
352
+
353
+ CREATE TRIGGER update_roles_updated_at BEFORE UPDATE ON roles
354
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
355
+
356
+ CREATE TRIGGER update_permissions_updated_at BEFORE UPDATE ON permissions
357
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
358
+
359
+ CREATE TRIGGER update_user_attributes_updated_at BEFORE UPDATE ON user_attributes
360
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
361
+
362
+ CREATE TRIGGER update_resource_attributes_updated_at BEFORE UPDATE ON resource_attributes
363
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
364
+
365
+ -- Cache cleanup function
366
+ CREATE OR REPLACE FUNCTION cleanup_expired_cache()
367
+ RETURNS INTEGER AS $$
368
+ DECLARE
369
+ deleted_count INTEGER;
370
+ BEGIN
371
+ DELETE FROM permission_cache WHERE expires_at < CURRENT_TIMESTAMP;
372
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
373
+ RETURN deleted_count;
374
+ END;
375
+ $$ LANGUAGE plpgsql;
376
+
377
+ -- Session cleanup function
378
+ CREATE OR REPLACE FUNCTION cleanup_expired_sessions()
379
+ RETURNS INTEGER AS $$
380
+ DECLARE
381
+ deleted_count INTEGER;
382
+ BEGIN
383
+ DELETE FROM user_sessions WHERE expires_at < CURRENT_TIMESTAMP;
384
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
385
+ RETURN deleted_count;
386
+ END;
387
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,249 @@
1
+ """Enhanced tenant isolation utilities for admin nodes.
2
+
3
+ This module provides robust tenant isolation mechanisms to ensure that
4
+ multi-tenant operations properly enforce data boundaries and prevent
5
+ cross-tenant access.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Any, Dict, List, Optional, Set
11
+
12
+ from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class TenantContext:
19
+ """Represents the context for a specific tenant."""
20
+
21
+ tenant_id: str
22
+ permissions: Set[str]
23
+ user_ids: Set[str]
24
+ role_ids: Set[str]
25
+ resource_prefixes: Set[str]
26
+
27
+
28
+ class TenantIsolationManager:
29
+ """Manages tenant isolation for admin operations."""
30
+
31
+ def __init__(self, db_node):
32
+ """
33
+ Initialize tenant isolation manager.
34
+
35
+ Args:
36
+ db_node: Database node for tenant context queries
37
+ """
38
+ self.db_node = db_node
39
+ self._tenant_cache = {}
40
+
41
+ def get_tenant_context(self, tenant_id: str) -> TenantContext:
42
+ """
43
+ Get the context for a specific tenant.
44
+
45
+ Args:
46
+ tenant_id: The tenant ID
47
+
48
+ Returns:
49
+ TenantContext with tenant-specific data
50
+ """
51
+ if tenant_id not in self._tenant_cache:
52
+ self._tenant_cache[tenant_id] = self._load_tenant_context(tenant_id)
53
+
54
+ return self._tenant_cache[tenant_id]
55
+
56
+ def _load_tenant_context(self, tenant_id: str) -> TenantContext:
57
+ """Load tenant context from database."""
58
+ # Get all users for this tenant
59
+ users_query = """
60
+ SELECT user_id FROM users WHERE tenant_id = $1 AND status = 'active'
61
+ """
62
+ users_result = self.db_node.run(
63
+ query=users_query, parameters=[tenant_id], result_format="dict"
64
+ )
65
+ user_ids = {row["user_id"] for row in users_result.get("data", [])}
66
+
67
+ # Get all roles for this tenant
68
+ roles_query = """
69
+ SELECT role_id FROM roles WHERE tenant_id = $1 AND is_active = true
70
+ """
71
+ roles_result = self.db_node.run(
72
+ query=roles_query, parameters=[tenant_id], result_format="dict"
73
+ )
74
+ role_ids = {row["role_id"] for row in roles_result.get("data", [])}
75
+
76
+ # Get all permissions for this tenant (from roles)
77
+ permissions_query = """
78
+ SELECT DISTINCT unnest(
79
+ CASE
80
+ WHEN jsonb_typeof(permissions) = 'array'
81
+ THEN ARRAY(SELECT jsonb_array_elements_text(permissions))
82
+ ELSE ARRAY[]::text[]
83
+ END
84
+ ) as permission
85
+ FROM roles
86
+ WHERE tenant_id = $1 AND is_active = true
87
+ """
88
+ permissions_result = self.db_node.run(
89
+ query=permissions_query, parameters=[tenant_id], result_format="dict"
90
+ )
91
+ permissions = {row["permission"] for row in permissions_result.get("data", [])}
92
+
93
+ # Create resource prefixes for this tenant
94
+ resource_prefixes = {f"{tenant_id}:*", "*"}
95
+
96
+ return TenantContext(
97
+ tenant_id=tenant_id,
98
+ permissions=permissions,
99
+ user_ids=user_ids,
100
+ role_ids=role_ids,
101
+ resource_prefixes=resource_prefixes,
102
+ )
103
+
104
+ def validate_user_tenant_access(self, user_id: str, target_tenant_id: str) -> bool:
105
+ """
106
+ Validate that a user has access within a specific tenant.
107
+
108
+ Args:
109
+ user_id: The user ID to check
110
+ target_tenant_id: The tenant being accessed
111
+
112
+ Returns:
113
+ True if access is allowed, False otherwise
114
+ """
115
+ tenant_context = self.get_tenant_context(target_tenant_id)
116
+ return user_id in tenant_context.user_ids
117
+
118
+ def validate_role_tenant_access(self, role_id: str, target_tenant_id: str) -> bool:
119
+ """
120
+ Validate that a role belongs to a specific tenant.
121
+
122
+ Args:
123
+ role_id: The role ID to check
124
+ target_tenant_id: The tenant being accessed
125
+
126
+ Returns:
127
+ True if role belongs to tenant, False otherwise
128
+ """
129
+ tenant_context = self.get_tenant_context(target_tenant_id)
130
+ return role_id in tenant_context.role_ids
131
+
132
+ def check_cross_tenant_permission(
133
+ self,
134
+ user_id: str,
135
+ user_tenant_id: str,
136
+ resource_tenant_id: str,
137
+ permission: str,
138
+ ) -> bool:
139
+ """
140
+ Check if a user from one tenant can access resources in another tenant.
141
+
142
+ Args:
143
+ user_id: The user attempting access
144
+ user_tenant_id: The tenant the user belongs to
145
+ resource_tenant_id: The tenant of the resource being accessed
146
+ permission: The permission being requested
147
+
148
+ Returns:
149
+ True if cross-tenant access is allowed, False otherwise
150
+ """
151
+ # For now, enforce strict tenant isolation
152
+ # Users can only access resources in their own tenant
153
+ if user_tenant_id != resource_tenant_id:
154
+ logger.debug(
155
+ f"Cross-tenant access denied: user {user_id} from {user_tenant_id} "
156
+ f"attempting to access {resource_tenant_id}"
157
+ )
158
+ return False
159
+
160
+ # Same tenant access - check if user exists in tenant
161
+ return self.validate_user_tenant_access(user_id, resource_tenant_id)
162
+
163
+ def enforce_tenant_isolation(
164
+ self, user_id: str, user_tenant_id: str, operation_tenant_id: str
165
+ ) -> None:
166
+ """
167
+ Enforce tenant isolation for an operation.
168
+
169
+ Args:
170
+ user_id: The user performing the operation
171
+ user_tenant_id: The tenant the user belongs to
172
+ operation_tenant_id: The tenant context for the operation
173
+
174
+ Raises:
175
+ NodeValidationError: If tenant isolation is violated
176
+ """
177
+ if not self.check_cross_tenant_permission(
178
+ user_id, user_tenant_id, operation_tenant_id, "access"
179
+ ):
180
+ raise NodeValidationError(
181
+ f"Tenant isolation violation: user {user_id} from tenant "
182
+ f"{user_tenant_id} cannot access tenant {operation_tenant_id}"
183
+ )
184
+
185
+ def get_tenant_scoped_permission(
186
+ self, permission: str, tenant_id: str, resource_id: Optional[str] = None
187
+ ) -> str:
188
+ """
189
+ Create a tenant-scoped permission string.
190
+
191
+ Args:
192
+ permission: Base permission (e.g., "read", "write")
193
+ tenant_id: Tenant ID
194
+ resource_id: Optional resource ID
195
+
196
+ Returns:
197
+ Tenant-scoped permission string
198
+ """
199
+ if resource_id:
200
+ return f"{tenant_id}:{resource_id}:{permission}"
201
+ else:
202
+ return f"{tenant_id}:*:{permission}"
203
+
204
+ def clear_tenant_cache(self, tenant_id: Optional[str] = None) -> None:
205
+ """
206
+ Clear the tenant context cache.
207
+
208
+ Args:
209
+ tenant_id: Specific tenant to clear, or None to clear all
210
+ """
211
+ if tenant_id:
212
+ self._tenant_cache.pop(tenant_id, None)
213
+ else:
214
+ self._tenant_cache.clear()
215
+
216
+ logger.debug(f"Cleared tenant cache for {tenant_id or 'all tenants'}")
217
+
218
+
219
+ def enforce_tenant_boundary(tenant_id_param: str = "tenant_id"):
220
+ """
221
+ Decorator to enforce tenant boundaries on admin node methods.
222
+
223
+ Args:
224
+ tenant_id_param: Name of the parameter containing the tenant ID
225
+ """
226
+
227
+ def decorator(func):
228
+ def wrapper(self, *args, **kwargs):
229
+ # Extract tenant ID from parameters
230
+ tenant_id = kwargs.get(tenant_id_param)
231
+ if not tenant_id:
232
+ raise NodeValidationError(
233
+ f"Missing required parameter: {tenant_id_param}"
234
+ )
235
+
236
+ # Create tenant isolation manager if not exists
237
+ if not hasattr(self, "_tenant_isolation"):
238
+ self._tenant_isolation = TenantIsolationManager(self._db_node)
239
+
240
+ # Perform the operation within tenant context
241
+ try:
242
+ return func(self, *args, **kwargs)
243
+ except Exception as e:
244
+ logger.error(f"Tenant-scoped operation failed for {tenant_id}: {e}")
245
+ raise
246
+
247
+ return wrapper
248
+
249
+ return decorator