kailash 0.6.1__py3-none-any.whl → 0.6.3__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.
Files changed (41) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/core/actors/connection_actor.py +3 -3
  3. kailash/gateway/api.py +7 -5
  4. kailash/gateway/enhanced_gateway.py +1 -1
  5. kailash/{mcp → mcp_server}/__init__.py +12 -7
  6. kailash/{mcp → mcp_server}/ai_registry_server.py +2 -2
  7. kailash/{mcp/server_enhanced.py → mcp_server/server.py} +231 -48
  8. kailash/{mcp → mcp_server}/servers/ai_registry.py +2 -2
  9. kailash/{mcp → mcp_server}/utils/__init__.py +1 -6
  10. kailash/middleware/auth/access_control.py +5 -5
  11. kailash/middleware/gateway/checkpoint_manager.py +45 -8
  12. kailash/middleware/mcp/client_integration.py +1 -1
  13. kailash/middleware/mcp/enhanced_server.py +2 -2
  14. kailash/nodes/admin/permission_check.py +110 -30
  15. kailash/nodes/admin/schema.sql +387 -0
  16. kailash/nodes/admin/tenant_isolation.py +249 -0
  17. kailash/nodes/admin/transaction_utils.py +244 -0
  18. kailash/nodes/admin/user_management.py +37 -9
  19. kailash/nodes/ai/ai_providers.py +55 -3
  20. kailash/nodes/ai/iterative_llm_agent.py +1 -1
  21. kailash/nodes/ai/llm_agent.py +118 -16
  22. kailash/nodes/data/sql.py +24 -0
  23. kailash/resources/registry.py +6 -0
  24. kailash/runtime/async_local.py +7 -0
  25. kailash/utils/export.py +152 -0
  26. kailash/workflow/builder.py +42 -0
  27. kailash/workflow/graph.py +86 -17
  28. kailash/workflow/templates.py +4 -9
  29. {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/METADATA +3 -2
  30. {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/RECORD +40 -38
  31. kailash/mcp/server.py +0 -292
  32. /kailash/{mcp → mcp_server}/client.py +0 -0
  33. /kailash/{mcp → mcp_server}/client_new.py +0 -0
  34. /kailash/{mcp → mcp_server}/utils/cache.py +0 -0
  35. /kailash/{mcp → mcp_server}/utils/config.py +0 -0
  36. /kailash/{mcp → mcp_server}/utils/formatters.py +0 -0
  37. /kailash/{mcp → mcp_server}/utils/metrics.py +0 -0
  38. {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/WHEEL +0 -0
  39. {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/entry_points.txt +0 -0
  40. {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/licenses/LICENSE +0 -0
  41. {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/top_level.txt +0 -0
@@ -721,36 +721,67 @@ class PermissionCheckNode(Node):
721
721
  }
722
722
 
723
723
  def _get_user_context(self, user_id: str, tenant_id: str) -> Optional[UserContext]:
724
- """Get user context for permission evaluation."""
725
- # Query user data from unified admin schema
726
- query = """
727
- SELECT user_id, email, roles, attributes, status, tenant_id
724
+ """Get user context for permission evaluation with strict tenant isolation."""
725
+ # Query user data and assigned roles from unified admin schema
726
+ user_query = """
727
+ SELECT user_id, email, attributes, status, tenant_id
728
728
  FROM users
729
729
  WHERE user_id = $1 AND tenant_id = $2 AND status = 'active'
730
730
  """
731
731
 
732
+ # Get assigned roles from user_role_assignments table with strict tenant isolation
733
+ roles_query = """
734
+ SELECT role_id
735
+ FROM user_role_assignments
736
+ WHERE user_id = $1 AND tenant_id = $2 AND is_active = true
737
+ """
738
+
732
739
  try:
733
- result = self._db_node.run(
734
- query=query, parameters=[user_id, tenant_id], result_format="dict"
740
+ # Get user data - strict tenant check
741
+ user_result = self._db_node.run(
742
+ query=user_query, parameters=[user_id, tenant_id], result_format="dict"
735
743
  )
736
744
 
737
- # Handle the corrected result structure
738
- user_rows = result.get("data", [])
745
+ user_rows = user_result.get("data", [])
739
746
  if not user_rows:
747
+ # User not found in this tenant - strict tenant isolation
748
+ self.logger.debug(f"User {user_id} not found in tenant {tenant_id}")
740
749
  return None
741
750
 
742
751
  user_data = user_rows[0]
743
752
 
753
+ # Verify tenant isolation - ensure user belongs to the requested tenant
754
+ if user_data.get("tenant_id") != tenant_id:
755
+ self.logger.warning(
756
+ f"Tenant isolation violation: User {user_id} belongs to {user_data.get('tenant_id')} but permission check requested for {tenant_id}"
757
+ )
758
+ return None
759
+
760
+ # Get assigned roles - also with strict tenant isolation
761
+ roles_result = self._db_node.run(
762
+ query=roles_query, parameters=[user_id, tenant_id], result_format="dict"
763
+ )
764
+
765
+ role_rows = roles_result.get("data", [])
766
+ assigned_roles = [row["role_id"] for row in role_rows]
767
+
768
+ # Log for debugging tenant isolation
769
+ self.logger.debug(
770
+ f"User {user_id} in tenant {tenant_id} has roles: {assigned_roles}"
771
+ )
772
+
744
773
  return UserContext(
745
774
  user_id=user_data["user_id"],
746
775
  tenant_id=user_data["tenant_id"],
747
776
  email=user_data["email"],
748
- roles=user_data.get("roles", []),
777
+ roles=assigned_roles,
749
778
  attributes=user_data.get("attributes", {}),
750
779
  )
751
780
  except Exception as e:
752
781
  # Log the error and return None to indicate user not found
753
- self.logger.warning(f"Failed to get user context for {user_id}: {e}")
782
+ self.logger.warning(
783
+ f"Failed to get user context for {user_id} in tenant {tenant_id}: {e}"
784
+ )
754
785
  return None
755
786
 
756
787
  def _check_rbac_permission(
@@ -826,39 +857,57 @@ class PermissionCheckNode(Node):
826
857
  return permissions
827
858
 
828
859
  def _get_role_permissions(self, role_id: str, tenant_id: str) -> Set[str]:
829
- """Get permissions for a specific role including inherited permissions."""
830
- # Query role and its hierarchy
860
+ """Get permissions for a specific role including inherited permissions with strict tenant isolation."""
861
+ # Query role and its hierarchy with strict tenant boundaries
831
862
  query = """
832
863
  WITH RECURSIVE role_hierarchy AS (
833
- SELECT role_id, permissions, parent_roles
864
+ SELECT role_id, permissions, parent_roles, tenant_id
834
865
  FROM roles
835
866
  WHERE role_id = $1 AND tenant_id = $2 AND is_active = true
836
867
 
837
868
  UNION ALL
838
869
 
839
- SELECT r.role_id, r.permissions, r.parent_roles
870
+ SELECT r.role_id, r.permissions, r.parent_roles, r.tenant_id
840
871
  FROM roles r
841
872
  JOIN role_hierarchy rh ON r.role_id = ANY(
842
873
  SELECT jsonb_array_elements_text(rh.parent_roles)
843
874
  )
844
- WHERE r.tenant_id = $2 AND r.is_active = true
875
+ WHERE r.tenant_id = $3 AND r.is_active = true
845
876
  )
846
877
  SELECT DISTINCT unnest(
847
878
  CASE
848
879
  WHEN jsonb_typeof(permissions) = 'array'
849
880
  THEN ARRAY(SELECT jsonb_array_elements_text(permissions))
881
+ WHEN permissions IS NOT NULL AND permissions::text != 'null'
882
+ THEN ARRAY[permissions::text]
850
883
  ELSE ARRAY[]::text[]
851
884
  END
852
885
  ) as permission
853
886
  FROM role_hierarchy
887
+ WHERE tenant_id = $4
854
888
  """
855
889
 
856
- result = self._db_node.run(
857
- query=query, parameters=[role_id, tenant_id], result_format="dict"
858
- )
859
- permission_rows = result.get("data", [])
890
+ try:
891
+ result = self._db_node.run(
892
+ query=query,
893
+ parameters=[role_id, tenant_id, tenant_id, tenant_id],
894
+ result_format="dict",
895
+ )
896
+ permission_rows = result.get("data", [])
860
897
 
861
- return {row["permission"] for row in permission_rows}
898
+ permissions = {
899
+ row["permission"] for row in permission_rows if row["permission"]
900
+ }
901
+ self.logger.debug(
902
+ f"Role {role_id} in tenant {tenant_id} has permissions: {permissions}"
903
+ )
904
+
905
+ return permissions
906
+ except Exception as e:
907
+ self.logger.warning(
908
+ f"Failed to get permissions for role {role_id} in tenant {tenant_id}: {e}"
909
+ )
910
+ return set()
862
911
 
863
912
  def _build_permission_explanation(
864
913
  self,
@@ -1189,23 +1238,54 @@ class PermissionCheckNode(Node):
1189
1238
  }
1190
1239
 
1191
1240
  def _get_role_direct_permissions(self, role_id: str, tenant_id: str) -> Set[str]:
1192
- """Get direct permissions for a role (no inheritance)."""
1241
+ """Get direct permissions for a role (no inheritance) with proper format handling."""
1193
1242
  query = """
1194
1243
  SELECT permissions
1195
1244
  FROM roles
1196
1245
  WHERE role_id = $1 AND tenant_id = $2 AND is_active = true
1197
1246
  """
1198
1247
 
1199
- result = self._db_node.run(
1200
- query=query, parameters=[role_id, tenant_id], result_format="dict"
1201
- )
1202
- role_rows = result.get("data", [])
1203
- role_data = role_rows[0] if role_rows else None
1248
+ try:
1249
+ result = self._db_node.run(
1250
+ query=query, parameters=[role_id, tenant_id], result_format="dict"
1251
+ )
1252
+ role_rows = result.get("data", [])
1253
+ role_data = role_rows[0] if role_rows else None
1204
1254
 
1205
- if not role_data:
1206
- return set()
1255
+ if not role_data:
1256
+ self.logger.debug(f"Role {role_id} not found in tenant {tenant_id}")
1257
+ return set()
1207
1258
 
1208
- return set(role_data.get("permissions", []))
1259
+ permissions_data = role_data.get("permissions", [])
1260
+
1261
+ # Handle different permission storage formats
1262
+ if isinstance(permissions_data, list):
1263
+ permissions = set(permissions_data)
1264
+ elif isinstance(permissions_data, str):
1265
+ try:
1266
+ # Try to parse as JSON array
1267
+ import json
1268
+
1269
+ parsed = json.loads(permissions_data)
1270
+ permissions = (
1271
+ set(parsed) if isinstance(parsed, list) else {permissions_data}
1272
+ )
1273
+ except (json.JSONDecodeError, TypeError):
1274
+ # Treat as single permission string
1275
+ permissions = {permissions_data} if permissions_data else set()
1276
+ else:
1277
+ permissions = set()
1278
+
1279
+ self.logger.debug(
1280
+ f"Role {role_id} direct permissions in tenant {tenant_id}: {permissions}"
1281
+ )
1282
+ return permissions
1283
+
1284
+ except Exception as e:
1285
+ self.logger.warning(
1286
+ f"Failed to get direct permissions for role {role_id} in tenant {tenant_id}: {e}"
1287
+ )
1288
+ return set()
1209
1289
 
1210
1290
  def _explain_permission(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1211
1291
  """Provide detailed explanation of permission logic."""
@@ -1625,7 +1705,7 @@ class PermissionCheckNode(Node):
1625
1705
  audit_query = """
1626
1706
  INSERT INTO admin_audit_log (
1627
1707
  user_id, action, resource_type, resource_id,
1628
- operation, details, success, tenant_id, created_at
1708
+ operation, context, success, tenant_id, created_at
1629
1709
  ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
1630
1710
  """
1631
1711
 
@@ -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;