kailash 0.4.2__py3-none-any.whl → 0.5.0__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/middleware/database/repositories.py +3 -1
- kailash/nodes/admin/audit_log.py +364 -6
- kailash/nodes/admin/user_management.py +1006 -20
- kailash/nodes/api/http.py +95 -71
- kailash/nodes/base.py +281 -164
- kailash/nodes/base_async.py +30 -31
- kailash/nodes/data/async_sql.py +3 -22
- kailash/utils/resource_manager.py +420 -0
- kailash/workflow/builder.py +93 -10
- kailash/workflow/cyclic_runner.py +4 -25
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/METADATA +6 -4
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/RECORD +16 -15
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/WHEEL +0 -0
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.4.2.dist-info → kailash-0.5.0.dist-info}/top_level.txt +0 -0
@@ -56,7 +56,9 @@ class BaseRepository:
|
|
56
56
|
"""Execute database query using SDK node."""
|
57
57
|
try:
|
58
58
|
if self.use_async:
|
59
|
-
result = await self.db_node.
|
59
|
+
result = await self.db_node.execute_async(
|
60
|
+
query=query, params=params or {}
|
61
|
+
)
|
60
62
|
else:
|
61
63
|
result = self.db_node.execute(query=query, params=params or {})
|
62
64
|
|
kailash/nodes/admin/audit_log.py
CHANGED
@@ -18,8 +18,9 @@ Features:
|
|
18
18
|
|
19
19
|
import hashlib
|
20
20
|
import json
|
21
|
+
import uuid
|
21
22
|
from dataclasses import dataclass
|
22
|
-
from datetime import UTC, datetime, timedelta
|
23
|
+
from datetime import UTC, datetime, timedelta, timezone
|
23
24
|
from enum import Enum
|
24
25
|
from typing import Any, Dict, List, Optional, Union
|
25
26
|
|
@@ -775,20 +776,377 @@ class EnterpriseAuditLogNode(Node):
|
|
775
776
|
# Additional operations would follow similar patterns
|
776
777
|
def _export_logs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
777
778
|
"""Export audit logs in various formats."""
|
778
|
-
|
779
|
+
format_type = inputs.get("export_format", "json")
|
780
|
+
query_filters = inputs.get("query_filters", {})
|
781
|
+
date_range = inputs.get("date_range", {})
|
782
|
+
|
783
|
+
# Query logs
|
784
|
+
query_result = self._query_logs(
|
785
|
+
{
|
786
|
+
"query_filters": query_filters,
|
787
|
+
"date_range": date_range,
|
788
|
+
"pagination": {"page": 1, "size": 10000}, # Export all matching records
|
789
|
+
}
|
790
|
+
)
|
791
|
+
|
792
|
+
logs = query_result.get("logs", [])
|
793
|
+
|
794
|
+
if format_type == "json":
|
795
|
+
export_data = {
|
796
|
+
"export_date": datetime.now(timezone.utc).isoformat(),
|
797
|
+
"total_records": len(logs),
|
798
|
+
"filters": query_filters,
|
799
|
+
"date_range": date_range,
|
800
|
+
"logs": logs,
|
801
|
+
}
|
802
|
+
filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
803
|
+
|
804
|
+
elif format_type == "csv":
|
805
|
+
# Convert to CSV format
|
806
|
+
import csv
|
807
|
+
import io
|
808
|
+
|
809
|
+
output = io.StringIO()
|
810
|
+
if logs:
|
811
|
+
writer = csv.DictWriter(output, fieldnames=logs[0].keys())
|
812
|
+
writer.writeheader()
|
813
|
+
writer.writerows(logs)
|
814
|
+
|
815
|
+
export_data = output.getvalue()
|
816
|
+
filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
817
|
+
|
818
|
+
elif format_type == "pdf":
|
819
|
+
# For PDF, we'll return structured data that can be rendered
|
820
|
+
export_data = {
|
821
|
+
"title": "Audit Log Report",
|
822
|
+
"generated_date": datetime.now(timezone.utc).isoformat(),
|
823
|
+
"summary": {
|
824
|
+
"total_records": len(logs),
|
825
|
+
"date_range": date_range,
|
826
|
+
"filters": query_filters,
|
827
|
+
},
|
828
|
+
"logs": logs,
|
829
|
+
}
|
830
|
+
filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
831
|
+
|
832
|
+
return {
|
833
|
+
"success": True,
|
834
|
+
"filename": filename,
|
835
|
+
"format": format_type,
|
836
|
+
"record_count": len(logs),
|
837
|
+
"export_data": export_data,
|
838
|
+
}
|
779
839
|
|
780
840
|
def _archive_logs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
781
841
|
"""Archive old audit logs for long-term storage."""
|
782
|
-
|
842
|
+
archive_days = inputs.get("archive_older_than_days", 90)
|
843
|
+
archive_path = inputs.get("archive_path", "/archives/audit_logs")
|
844
|
+
tenant_id = inputs.get("tenant_id")
|
845
|
+
|
846
|
+
# Calculate cutoff date
|
847
|
+
cutoff_date = datetime.now(timezone.utc) - timedelta(days=archive_days)
|
848
|
+
|
849
|
+
# Query old logs
|
850
|
+
query_result = self._query_logs(
|
851
|
+
{
|
852
|
+
"date_range": {"end": cutoff_date.isoformat()},
|
853
|
+
"tenant_id": tenant_id,
|
854
|
+
"pagination": {"page": 1, "size": 10000},
|
855
|
+
}
|
856
|
+
)
|
857
|
+
|
858
|
+
logs_to_archive = query_result.get("logs", [])
|
859
|
+
|
860
|
+
if not logs_to_archive:
|
861
|
+
return {
|
862
|
+
"success": True,
|
863
|
+
"message": "No logs to archive",
|
864
|
+
"archived_count": 0,
|
865
|
+
}
|
866
|
+
|
867
|
+
# Create archive
|
868
|
+
archive_filename = f"audit_archive_{cutoff_date.strftime('%Y%m%d')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
869
|
+
archive_data = {
|
870
|
+
"archive_date": datetime.now(timezone.utc).isoformat(),
|
871
|
+
"cutoff_date": cutoff_date.isoformat(),
|
872
|
+
"total_records": len(logs_to_archive),
|
873
|
+
"tenant_id": tenant_id,
|
874
|
+
"logs": logs_to_archive,
|
875
|
+
}
|
876
|
+
|
877
|
+
# In a real implementation, this would save to cloud storage or archive system
|
878
|
+
archive_location = f"{archive_path}/{archive_filename}"
|
879
|
+
|
880
|
+
# Delete archived logs from main database
|
881
|
+
log_ids = [log.get("id") for log in logs_to_archive if log.get("id")]
|
882
|
+
|
883
|
+
if log_ids:
|
884
|
+
# Delete logs
|
885
|
+
delete_query = """
|
886
|
+
DELETE FROM audit_logs
|
887
|
+
WHERE id IN (%s)
|
888
|
+
""" % ",".join(
|
889
|
+
["?" for _ in log_ids]
|
890
|
+
)
|
891
|
+
|
892
|
+
if tenant_id:
|
893
|
+
delete_query += " AND tenant_id = ?"
|
894
|
+
log_ids.append(tenant_id)
|
895
|
+
|
896
|
+
self._ensure_db_node(inputs)
|
897
|
+
self._db_node.execute(query=delete_query, params=log_ids)
|
898
|
+
|
899
|
+
return {
|
900
|
+
"success": True,
|
901
|
+
"archived_count": len(logs_to_archive),
|
902
|
+
"archive_location": archive_location,
|
903
|
+
"archive_filename": archive_filename,
|
904
|
+
"cutoff_date": cutoff_date.isoformat(),
|
905
|
+
"message": f"Archived {len(logs_to_archive)} logs older than {archive_days} days",
|
906
|
+
}
|
783
907
|
|
784
908
|
def _delete_logs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
785
909
|
"""Delete old audit logs based on retention policy."""
|
786
|
-
|
910
|
+
retention_days = inputs.get("retention_days", 365)
|
911
|
+
tenant_id = inputs.get("tenant_id")
|
912
|
+
dry_run = inputs.get("dry_run", False)
|
913
|
+
|
914
|
+
# Calculate cutoff date
|
915
|
+
cutoff_date = datetime.now(timezone.utc) - timedelta(days=retention_days)
|
916
|
+
|
917
|
+
# First count logs to be deleted
|
918
|
+
count_query = """
|
919
|
+
SELECT COUNT(*) as count FROM audit_logs
|
920
|
+
WHERE created_at < ?
|
921
|
+
"""
|
922
|
+
params = [cutoff_date.isoformat()]
|
923
|
+
|
924
|
+
if tenant_id:
|
925
|
+
count_query += " AND tenant_id = ?"
|
926
|
+
params.append(tenant_id)
|
927
|
+
|
928
|
+
self._ensure_db_node(inputs)
|
929
|
+
count_result = self._db_node.execute(query=count_query, params=params)
|
930
|
+
total_to_delete = count_result.get("rows", [{}])[0].get("count", 0)
|
931
|
+
|
932
|
+
if dry_run:
|
933
|
+
return {
|
934
|
+
"success": True,
|
935
|
+
"dry_run": True,
|
936
|
+
"would_delete": total_to_delete,
|
937
|
+
"cutoff_date": cutoff_date.isoformat(),
|
938
|
+
"message": f"Dry run: Would delete {total_to_delete} logs older than {retention_days} days",
|
939
|
+
}
|
940
|
+
|
941
|
+
if total_to_delete == 0:
|
942
|
+
return {"success": True, "deleted_count": 0, "message": "No logs to delete"}
|
943
|
+
|
944
|
+
# Delete logs in batches to avoid locking
|
945
|
+
batch_size = 1000
|
946
|
+
deleted_total = 0
|
947
|
+
|
948
|
+
while deleted_total < total_to_delete:
|
949
|
+
delete_query = f"""
|
950
|
+
DELETE FROM audit_logs
|
951
|
+
WHERE id IN (
|
952
|
+
SELECT id FROM audit_logs
|
953
|
+
WHERE created_at < ?
|
954
|
+
{' AND tenant_id = ?' if tenant_id else ''}
|
955
|
+
LIMIT {batch_size}
|
956
|
+
)
|
957
|
+
"""
|
958
|
+
|
959
|
+
delete_params = [cutoff_date.isoformat()]
|
960
|
+
if tenant_id:
|
961
|
+
delete_params.append(tenant_id)
|
962
|
+
|
963
|
+
result = self._db_node.execute(query=delete_query, params=delete_params)
|
964
|
+
batch_deleted = result.get("rows_affected", 0)
|
965
|
+
deleted_total += batch_deleted
|
966
|
+
|
967
|
+
if batch_deleted == 0:
|
968
|
+
break
|
969
|
+
|
970
|
+
return {
|
971
|
+
"success": True,
|
972
|
+
"deleted_count": deleted_total,
|
973
|
+
"cutoff_date": cutoff_date.isoformat(),
|
974
|
+
"retention_days": retention_days,
|
975
|
+
"message": f"Deleted {deleted_total} logs older than {retention_days} days",
|
976
|
+
}
|
787
977
|
|
788
978
|
def _get_statistics(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
789
979
|
"""Get audit log statistics and metrics."""
|
790
|
-
|
980
|
+
tenant_id = inputs.get("tenant_id")
|
981
|
+
date_range = inputs.get("date_range", {})
|
982
|
+
group_by = inputs.get(
|
983
|
+
"group_by", ["event_type", "severity"]
|
984
|
+
) # What to group statistics by
|
985
|
+
|
986
|
+
self._ensure_db_node(inputs)
|
987
|
+
|
988
|
+
# Build base WHERE clause
|
989
|
+
where_conditions = []
|
990
|
+
params = []
|
991
|
+
|
992
|
+
if tenant_id:
|
993
|
+
where_conditions.append("tenant_id = ?")
|
994
|
+
params.append(tenant_id)
|
995
|
+
|
996
|
+
if date_range:
|
997
|
+
if date_range.get("start"):
|
998
|
+
where_conditions.append("created_at >= ?")
|
999
|
+
params.append(date_range["start"])
|
1000
|
+
if date_range.get("end"):
|
1001
|
+
where_conditions.append("created_at <= ?")
|
1002
|
+
params.append(date_range["end"])
|
1003
|
+
|
1004
|
+
where_clause = (
|
1005
|
+
" WHERE " + " AND ".join(where_conditions) if where_conditions else ""
|
1006
|
+
)
|
1007
|
+
|
1008
|
+
# Get total count
|
1009
|
+
total_query = f"SELECT COUNT(*) as total FROM audit_logs{where_clause}"
|
1010
|
+
total_result = self._db_node.execute(query=total_query, params=params)
|
1011
|
+
total_count = total_result.get("rows", [{}])[0].get("total", 0)
|
1012
|
+
|
1013
|
+
# Get counts by severity
|
1014
|
+
severity_query = f"""
|
1015
|
+
SELECT severity, COUNT(*) as count
|
1016
|
+
FROM audit_logs{where_clause}
|
1017
|
+
GROUP BY severity
|
1018
|
+
"""
|
1019
|
+
severity_result = self._db_node.execute(query=severity_query, params=params)
|
1020
|
+
severity_counts = {
|
1021
|
+
row["severity"]: row["count"] for row in severity_result.get("rows", [])
|
1022
|
+
}
|
1023
|
+
|
1024
|
+
# Get counts by event type
|
1025
|
+
event_type_query = f"""
|
1026
|
+
SELECT event_type, COUNT(*) as count
|
1027
|
+
FROM audit_logs{where_clause}
|
1028
|
+
GROUP BY event_type
|
1029
|
+
ORDER BY count DESC
|
1030
|
+
LIMIT 20
|
1031
|
+
"""
|
1032
|
+
event_type_result = self._db_node.execute(query=event_type_query, params=params)
|
1033
|
+
event_type_counts = {
|
1034
|
+
row["event_type"]: row["count"] for row in event_type_result.get("rows", [])
|
1035
|
+
}
|
1036
|
+
|
1037
|
+
# Get hourly distribution for the date range
|
1038
|
+
hourly_query = f"""
|
1039
|
+
SELECT
|
1040
|
+
strftime('%Y-%m-%d %H:00:00', created_at) as hour,
|
1041
|
+
COUNT(*) as count
|
1042
|
+
FROM audit_logs{where_clause}
|
1043
|
+
GROUP BY hour
|
1044
|
+
ORDER BY hour DESC
|
1045
|
+
LIMIT 168
|
1046
|
+
""" # Last 7 days of hourly data
|
1047
|
+
hourly_result = self._db_node.execute(query=hourly_query, params=params)
|
1048
|
+
hourly_distribution = [
|
1049
|
+
{"hour": row["hour"], "count": row["count"]}
|
1050
|
+
for row in hourly_result.get("rows", [])
|
1051
|
+
]
|
1052
|
+
|
1053
|
+
# Get top users by activity
|
1054
|
+
user_activity_query = f"""
|
1055
|
+
SELECT user_id, COUNT(*) as action_count
|
1056
|
+
FROM audit_logs{where_clause}
|
1057
|
+
GROUP BY user_id
|
1058
|
+
ORDER BY action_count DESC
|
1059
|
+
LIMIT 10
|
1060
|
+
"""
|
1061
|
+
user_activity_result = self._db_node.execute(
|
1062
|
+
query=user_activity_query, params=params
|
1063
|
+
)
|
1064
|
+
top_users = [
|
1065
|
+
{"user_id": row["user_id"], "action_count": row["action_count"]}
|
1066
|
+
for row in user_activity_result.get("rows", [])
|
1067
|
+
]
|
1068
|
+
|
1069
|
+
# Get failed actions
|
1070
|
+
failed_query = f"""
|
1071
|
+
SELECT COUNT(*) as failed_count
|
1072
|
+
FROM audit_logs{where_clause}
|
1073
|
+
{' AND ' if where_clause else ' WHERE '}
|
1074
|
+
status = 'failed' OR severity = 'error'
|
1075
|
+
"""
|
1076
|
+
failed_params = params.copy()
|
1077
|
+
failed_result = self._db_node.execute(query=failed_query, params=failed_params)
|
1078
|
+
failed_count = failed_result.get("rows", [{}])[0].get("failed_count", 0)
|
1079
|
+
|
1080
|
+
statistics = {
|
1081
|
+
"total_events": total_count,
|
1082
|
+
"failed_events": failed_count,
|
1083
|
+
"success_rate": (
|
1084
|
+
((total_count - failed_count) / total_count * 100)
|
1085
|
+
if total_count > 0
|
1086
|
+
else 0
|
1087
|
+
),
|
1088
|
+
"severity_distribution": severity_counts,
|
1089
|
+
"event_type_distribution": event_type_counts,
|
1090
|
+
"hourly_distribution": hourly_distribution,
|
1091
|
+
"top_users": top_users,
|
1092
|
+
"date_range": date_range,
|
1093
|
+
"tenant_id": tenant_id,
|
1094
|
+
}
|
1095
|
+
|
1096
|
+
return {"success": True, "statistics": statistics}
|
791
1097
|
|
792
1098
|
def _monitor_realtime(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
793
1099
|
"""Monitor audit logs in real-time."""
|
794
|
-
|
1100
|
+
# This operation would typically set up a subscription or polling mechanism
|
1101
|
+
# For now, we'll return the latest events and configuration for real-time monitoring
|
1102
|
+
|
1103
|
+
tenant_id = inputs.get("tenant_id")
|
1104
|
+
event_types = inputs.get("event_types", []) # Filter by specific event types
|
1105
|
+
severity_filter = inputs.get("severity", AuditSeverity.INFO.value)
|
1106
|
+
polling_interval = inputs.get("polling_interval", 5) # seconds
|
1107
|
+
max_events = inputs.get("max_events", 100)
|
1108
|
+
|
1109
|
+
# Get latest events
|
1110
|
+
query_result = self._query_logs(
|
1111
|
+
{
|
1112
|
+
"tenant_id": tenant_id,
|
1113
|
+
"event_types": event_types,
|
1114
|
+
"severity": severity_filter,
|
1115
|
+
"pagination": {
|
1116
|
+
"page": 1,
|
1117
|
+
"size": max_events,
|
1118
|
+
"sort": [{"field": "created_at", "order": "desc"}],
|
1119
|
+
},
|
1120
|
+
}
|
1121
|
+
)
|
1122
|
+
|
1123
|
+
latest_events = query_result.get("logs", [])
|
1124
|
+
|
1125
|
+
# Create monitoring configuration
|
1126
|
+
monitor_config = {
|
1127
|
+
"monitor_id": str(uuid.uuid4()),
|
1128
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
1129
|
+
"filters": {
|
1130
|
+
"tenant_id": tenant_id,
|
1131
|
+
"event_types": event_types,
|
1132
|
+
"severity": severity_filter,
|
1133
|
+
},
|
1134
|
+
"polling_interval": polling_interval,
|
1135
|
+
"max_events": max_events,
|
1136
|
+
"status": "active",
|
1137
|
+
"last_poll": datetime.now(timezone.utc).isoformat(),
|
1138
|
+
"endpoint": f"/api/audit/monitor/{uuid.uuid4()}", # Webhook or WebSocket endpoint
|
1139
|
+
}
|
1140
|
+
|
1141
|
+
# In a real implementation, this would:
|
1142
|
+
# 1. Set up a WebSocket connection or Server-Sent Events stream
|
1143
|
+
# 2. Create database triggers or use change data capture
|
1144
|
+
# 3. Set up a message queue subscription
|
1145
|
+
|
1146
|
+
return {
|
1147
|
+
"success": True,
|
1148
|
+
"monitor_config": monitor_config,
|
1149
|
+
"latest_events": latest_events,
|
1150
|
+
"event_count": len(latest_events),
|
1151
|
+
"message": "Real-time monitoring configured. Use the endpoint for live updates.",
|
1152
|
+
}
|