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.
- kailash/__init__.py +1 -1
- kailash/core/actors/connection_actor.py +3 -3
- kailash/gateway/api.py +7 -5
- kailash/gateway/enhanced_gateway.py +1 -1
- kailash/{mcp → mcp_server}/__init__.py +12 -7
- kailash/{mcp → mcp_server}/ai_registry_server.py +2 -2
- kailash/{mcp/server_enhanced.py → mcp_server/server.py} +231 -48
- kailash/{mcp → mcp_server}/servers/ai_registry.py +2 -2
- kailash/{mcp → mcp_server}/utils/__init__.py +1 -6
- kailash/middleware/auth/access_control.py +5 -5
- kailash/middleware/gateway/checkpoint_manager.py +45 -8
- kailash/middleware/mcp/client_integration.py +1 -1
- kailash/middleware/mcp/enhanced_server.py +2 -2
- kailash/nodes/admin/permission_check.py +110 -30
- kailash/nodes/admin/schema.sql +387 -0
- kailash/nodes/admin/tenant_isolation.py +249 -0
- kailash/nodes/admin/transaction_utils.py +244 -0
- kailash/nodes/admin/user_management.py +37 -9
- kailash/nodes/ai/ai_providers.py +55 -3
- kailash/nodes/ai/iterative_llm_agent.py +1 -1
- kailash/nodes/ai/llm_agent.py +118 -16
- kailash/nodes/data/sql.py +24 -0
- kailash/resources/registry.py +6 -0
- kailash/runtime/async_local.py +7 -0
- kailash/utils/export.py +152 -0
- kailash/workflow/builder.py +42 -0
- kailash/workflow/graph.py +86 -17
- kailash/workflow/templates.py +4 -9
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/METADATA +3 -2
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/RECORD +40 -38
- kailash/mcp/server.py +0 -292
- /kailash/{mcp → mcp_server}/client.py +0 -0
- /kailash/{mcp → mcp_server}/client_new.py +0 -0
- /kailash/{mcp → mcp_server}/utils/cache.py +0 -0
- /kailash/{mcp → mcp_server}/utils/config.py +0 -0
- /kailash/{mcp → mcp_server}/utils/formatters.py +0 -0
- /kailash/{mcp → mcp_server}/utils/metrics.py +0 -0
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/WHEEL +0 -0
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
727
|
-
SELECT user_id, email,
|
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
|
-
|
734
|
-
|
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
|
-
|
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=
|
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(
|
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 = $
|
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
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
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
|
-
|
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
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
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
|
-
|
1206
|
-
|
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
|
-
|
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,
|
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;
|