|
|
|
|
@ -182,42 +182,149 @@ class SynchronizationApi(HierarchyApiView): |
|
|
|
|
'results': results |
|
|
|
|
}, status=207) # Multi-Status |
|
|
|
|
|
|
|
|
|
# def get(self, request, *args, **kwargs): |
|
|
|
|
# last_update_str = request.query_params.get('last_update') |
|
|
|
|
# device_id = request.query_params.get('device_id') |
|
|
|
|
|
|
|
|
|
# # print(f'last_update_str = {last_update_str}') |
|
|
|
|
# decoded_last_update = unquote(last_update_str) # Decodes %2B into + |
|
|
|
|
|
|
|
|
|
# # print(f'last_update_str = {last_update_str}') |
|
|
|
|
# # print(f'decoded_last_update = {decoded_last_update}') |
|
|
|
|
# if not decoded_last_update: |
|
|
|
|
# return Response({"error": "last_update parameter is required"}, status=status.HTTP_400_BAD_REQUEST) |
|
|
|
|
# try: |
|
|
|
|
# last_update = timezone.datetime.fromisoformat(decoded_last_update) |
|
|
|
|
# except ValueError: |
|
|
|
|
# return Response({"error": f"Invalid date format for last_update: {decoded_last_update}"}, status=status.HTTP_400_BAD_REQUEST) |
|
|
|
|
|
|
|
|
|
# # print(f'/data GET: {last_update}') |
|
|
|
|
|
|
|
|
|
# logs = self.query_model_logs(last_update, request.user, device_id) |
|
|
|
|
# print(f'>>> log count = {len(logs)}') |
|
|
|
|
|
|
|
|
|
# updates = defaultdict(dict) |
|
|
|
|
# deletions = defaultdict(list) |
|
|
|
|
# grants = defaultdict(dict) |
|
|
|
|
# revocations = defaultdict(list) |
|
|
|
|
# revocations_parents_organizer = HierarchyOrganizer() |
|
|
|
|
|
|
|
|
|
# # revocated_parents = defaultdict(dict) |
|
|
|
|
|
|
|
|
|
# last_log_date = None |
|
|
|
|
# for log in logs: |
|
|
|
|
|
|
|
|
|
# # log.retrieved() |
|
|
|
|
# # log.save() |
|
|
|
|
|
|
|
|
|
# # print(f'log date = {log.date}') |
|
|
|
|
# last_log_date = log.date |
|
|
|
|
# try: |
|
|
|
|
# if log.operation in ['POST', 'PUT']: |
|
|
|
|
# data = get_serialized_data(log.model_name, log.model_id) |
|
|
|
|
# updates[log.model_name][log.model_id] = data |
|
|
|
|
# elif log.operation == 'DELETE': |
|
|
|
|
# deletions[log.model_name].append({'model_id': log.model_id, 'store_id': log.store_id}) |
|
|
|
|
# elif log.operation == 'GRANT_ACCESS': |
|
|
|
|
|
|
|
|
|
# model = model_registry.get_model(log.model_name) |
|
|
|
|
# instance = model.objects.get(id=log.model_id) |
|
|
|
|
# serializer = get_serializer(instance, log.model_name) |
|
|
|
|
|
|
|
|
|
# grants[log.model_name][log.model_id] = serializer.data |
|
|
|
|
# self.add_children_recursively(instance, grants) |
|
|
|
|
# self.add_parents_recursively(instance, grants) |
|
|
|
|
# elif log.operation == 'REVOKE_ACCESS': |
|
|
|
|
# print(f'revoke access {log.model_id} - {log.store_id}') |
|
|
|
|
# revocations[log.model_name].append({ |
|
|
|
|
# 'model_id': log.model_id, |
|
|
|
|
# 'store_id': log.store_id |
|
|
|
|
# }) |
|
|
|
|
|
|
|
|
|
# # Get the model instance and add its parents to hierarchy |
|
|
|
|
# model = model_registry.get_model(log.model_name) |
|
|
|
|
# try: |
|
|
|
|
# instance = model.objects.get(id=log.model_id) |
|
|
|
|
# self.add_parents_with_hierarchy_organizer(instance, revocations_parents_organizer) |
|
|
|
|
# except model.DoesNotExist: |
|
|
|
|
# pass |
|
|
|
|
# except ObjectDoesNotExist: |
|
|
|
|
# pass |
|
|
|
|
|
|
|
|
|
# # Convert updates dict to list for each model |
|
|
|
|
# for model_name in updates: |
|
|
|
|
# updates[model_name] = list(updates[model_name].values()) |
|
|
|
|
|
|
|
|
|
# # Convert deletions set to list for each model |
|
|
|
|
# for model_name in deletions: |
|
|
|
|
# deletions[model_name] = deletions[model_name] |
|
|
|
|
|
|
|
|
|
# for model_name in grants: |
|
|
|
|
# grants[model_name] = list(grants[model_name].values()) |
|
|
|
|
|
|
|
|
|
# # for model_name in revocation_parents: |
|
|
|
|
# # revocation_parents[model_name] = list(revocation_parents[model_name].values()) |
|
|
|
|
|
|
|
|
|
# response_data = { |
|
|
|
|
# "updates": dict(updates), |
|
|
|
|
# "deletions": dict(deletions), |
|
|
|
|
# "grants": dict(grants), |
|
|
|
|
# "revocations": dict(revocations), |
|
|
|
|
# "revocation_parents": revocations_parents_organizer.get_organized_data(), |
|
|
|
|
# "date": last_log_date |
|
|
|
|
# } |
|
|
|
|
|
|
|
|
|
# # print(f'sync GET response. UP = {len(updates)} / DEL = {len(deletions)} / G = {len(grants)} / R = {len(revocations)}') |
|
|
|
|
# # print(f'sync GET response. response = {response_data}') |
|
|
|
|
# return Response(response_data, status=status.HTTP_200_OK) |
|
|
|
|
|
|
|
|
|
## GET |
|
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs): |
|
|
|
|
last_update_str = request.query_params.get('last_update') |
|
|
|
|
device_id = request.query_params.get('device_id') |
|
|
|
|
|
|
|
|
|
# print(f'last_update_str = {last_update_str}') |
|
|
|
|
decoded_last_update = unquote(last_update_str) # Decodes %2B into + |
|
|
|
|
|
|
|
|
|
# print(f'last_update_str = {last_update_str}') |
|
|
|
|
# print(f'decoded_last_update = {decoded_last_update}') |
|
|
|
|
if not decoded_last_update: |
|
|
|
|
return Response({"error": "last_update parameter is required"}, status=status.HTTP_400_BAD_REQUEST) |
|
|
|
|
try: |
|
|
|
|
last_update = timezone.datetime.fromisoformat(decoded_last_update) |
|
|
|
|
except ValueError: |
|
|
|
|
return Response({"error": f"Invalid date format for last_update: {decoded_last_update}"}, status=status.HTTP_400_BAD_REQUEST) |
|
|
|
|
|
|
|
|
|
# print(f'/data GET: {last_update}') |
|
|
|
|
return Response({"error": f"Invalid date format for last_update: {decoded_last_update}"}, |
|
|
|
|
status=status.HTTP_400_BAD_REQUEST) |
|
|
|
|
|
|
|
|
|
logs = self.query_model_logs(last_update, request.user, device_id) |
|
|
|
|
print(f'>>> log count = {len(logs)}') |
|
|
|
|
|
|
|
|
|
# First pass: Process logs to collect basic operations |
|
|
|
|
updates, deletions, grant_instances, revoke_info, last_log_date = self.process_logs(logs) |
|
|
|
|
|
|
|
|
|
# Second pass: Process hierarchies for grants and revocations |
|
|
|
|
grants = self.process_grants(grant_instances) |
|
|
|
|
revocations_parents_organizer = self.process_revocations(revoke_info) |
|
|
|
|
|
|
|
|
|
response_data = { |
|
|
|
|
"updates": dict(updates), |
|
|
|
|
"deletions": dict(deletions), |
|
|
|
|
"grants": dict(grants), |
|
|
|
|
"revocations": dict(revocations_parents_organizer[0]), |
|
|
|
|
"revocation_parents": revocations_parents_organizer[1].get_organized_data(), |
|
|
|
|
"date": last_log_date |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK) |
|
|
|
|
|
|
|
|
|
def process_logs(self, logs): |
|
|
|
|
"""Process logs to collect basic operations and handle grant/revoke efficiently.""" |
|
|
|
|
updates = defaultdict(dict) |
|
|
|
|
deletions = defaultdict(list) |
|
|
|
|
grants = defaultdict(dict) |
|
|
|
|
revocations = defaultdict(list) |
|
|
|
|
revocations_parents_organizer = HierarchyOrganizer() |
|
|
|
|
|
|
|
|
|
# revocated_parents = defaultdict(dict) |
|
|
|
|
grant_instances = defaultdict(dict) # {model_name: {model_id: instance}} |
|
|
|
|
revoke_info = defaultdict(list) # {model_name: [{model_id, store_id}]} |
|
|
|
|
|
|
|
|
|
last_log_date = None |
|
|
|
|
for log in logs: |
|
|
|
|
|
|
|
|
|
# log.retrieved() |
|
|
|
|
# log.save() |
|
|
|
|
|
|
|
|
|
# print(f'log date = {log.date}') |
|
|
|
|
for log in logs: |
|
|
|
|
last_log_date = log.date |
|
|
|
|
try: |
|
|
|
|
if log.operation in ['POST', 'PUT']: |
|
|
|
|
@ -226,28 +333,28 @@ class SynchronizationApi(HierarchyApiView): |
|
|
|
|
elif log.operation == 'DELETE': |
|
|
|
|
deletions[log.model_name].append({'model_id': log.model_id, 'store_id': log.store_id}) |
|
|
|
|
elif log.operation == 'GRANT_ACCESS': |
|
|
|
|
# Remove any existing revocations for this model_id |
|
|
|
|
self._remove_revocation(revoke_info, log.model_name, log.model_id) |
|
|
|
|
|
|
|
|
|
model = model_registry.get_model(log.model_name) |
|
|
|
|
instance = model.objects.get(id=log.model_id) |
|
|
|
|
serializer = get_serializer(instance, log.model_name) |
|
|
|
|
|
|
|
|
|
grants[log.model_name][log.model_id] = serializer.data |
|
|
|
|
self.add_children_recursively(instance, grants) |
|
|
|
|
self.add_parents_recursively(instance, grants) |
|
|
|
|
# Add to grant instances if not already there |
|
|
|
|
if log.model_id not in grant_instances[log.model_name]: |
|
|
|
|
model = model_registry.get_model(log.model_name) |
|
|
|
|
try: |
|
|
|
|
instance = model.objects.get(id=log.model_id) |
|
|
|
|
grant_instances[log.model_name][log.model_id] = instance |
|
|
|
|
except model.DoesNotExist: |
|
|
|
|
pass |
|
|
|
|
elif log.operation == 'REVOKE_ACCESS': |
|
|
|
|
print(f'revoke access {log.model_id} - {log.store_id}') |
|
|
|
|
revocations[log.model_name].append({ |
|
|
|
|
|
|
|
|
|
# Remove any existing grants for this model_id |
|
|
|
|
self._remove_grant(grant_instances, log.model_name, log.model_id) |
|
|
|
|
|
|
|
|
|
# Add to revocations |
|
|
|
|
revoke_info[log.model_name].append({ |
|
|
|
|
'model_id': log.model_id, |
|
|
|
|
'store_id': log.store_id |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
# Get the model instance and add its parents to hierarchy |
|
|
|
|
model = model_registry.get_model(log.model_name) |
|
|
|
|
try: |
|
|
|
|
instance = model.objects.get(id=log.model_id) |
|
|
|
|
self.add_parents_with_hierarchy_organizer(instance, revocations_parents_organizer) |
|
|
|
|
except model.DoesNotExist: |
|
|
|
|
pass |
|
|
|
|
except ObjectDoesNotExist: |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
@ -255,34 +362,72 @@ class SynchronizationApi(HierarchyApiView): |
|
|
|
|
for model_name in updates: |
|
|
|
|
updates[model_name] = list(updates[model_name].values()) |
|
|
|
|
|
|
|
|
|
# Convert deletions set to list for each model |
|
|
|
|
for model_name in deletions: |
|
|
|
|
deletions[model_name] = deletions[model_name] |
|
|
|
|
return updates, deletions, grant_instances, revoke_info, last_log_date |
|
|
|
|
|
|
|
|
|
def _remove_revocation(self, revoke_info, model_name, model_id): |
|
|
|
|
"""Remove any revocation entries for the specified model and ID.""" |
|
|
|
|
if model_name in revoke_info: |
|
|
|
|
revoke_info[model_name] = [ |
|
|
|
|
r for r in revoke_info[model_name] |
|
|
|
|
if r['model_id'] != model_id |
|
|
|
|
] |
|
|
|
|
# Clean up empty lists |
|
|
|
|
if not revoke_info[model_name]: |
|
|
|
|
del revoke_info[model_name] |
|
|
|
|
|
|
|
|
|
def _remove_grant(self, grant_instances, model_name, model_id): |
|
|
|
|
"""Remove any grant entries for the specified model and ID.""" |
|
|
|
|
if model_name in grant_instances and model_id in grant_instances[model_name]: |
|
|
|
|
del grant_instances[model_name][model_id] |
|
|
|
|
# Clean up empty dictionaries |
|
|
|
|
if not grant_instances[model_name]: |
|
|
|
|
del grant_instances[model_name] |
|
|
|
|
|
|
|
|
|
def process_grants(self, grant_instances): |
|
|
|
|
"""Process grants and their hierarchies.""" |
|
|
|
|
grants = defaultdict(dict) |
|
|
|
|
|
|
|
|
|
# Process each grant instance |
|
|
|
|
for model_name, instances in grant_instances.items(): |
|
|
|
|
for model_id, instance in instances.items(): |
|
|
|
|
serializer = get_serializer(instance, model_name) |
|
|
|
|
grants[model_name][model_id] = serializer.data |
|
|
|
|
|
|
|
|
|
# Add hierarchies only once per instance |
|
|
|
|
self.add_children_recursively(instance, grants) |
|
|
|
|
self.add_parents_recursively(instance, grants) |
|
|
|
|
|
|
|
|
|
# Convert to lists |
|
|
|
|
for model_name in grants: |
|
|
|
|
grants[model_name] = list(grants[model_name].values()) |
|
|
|
|
|
|
|
|
|
# for model_name in revocation_parents: |
|
|
|
|
# revocation_parents[model_name] = list(revocation_parents[model_name].values()) |
|
|
|
|
return grants |
|
|
|
|
|
|
|
|
|
response_data = { |
|
|
|
|
"updates": dict(updates), |
|
|
|
|
"deletions": dict(deletions), |
|
|
|
|
"grants": dict(grants), |
|
|
|
|
"revocations": dict(revocations), |
|
|
|
|
"revocation_parents": revocations_parents_organizer.get_organized_data(), |
|
|
|
|
"date": last_log_date |
|
|
|
|
} |
|
|
|
|
def process_revocations(self, revoke_info): |
|
|
|
|
"""Process revocations and their hierarchies.""" |
|
|
|
|
revocations = defaultdict(list) |
|
|
|
|
revocations_parents_organizer = HierarchyOrganizer() |
|
|
|
|
|
|
|
|
|
# print(f'sync GET response. UP = {len(updates)} / DEL = {len(deletions)} / G = {len(grants)} / R = {len(revocations)}') |
|
|
|
|
# print(f'sync GET response. response = {response_data}') |
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK) |
|
|
|
|
# First, collect all revocations |
|
|
|
|
for model_name, items in revoke_info.items(): |
|
|
|
|
revocations[model_name].extend(items) |
|
|
|
|
|
|
|
|
|
# Process parent hierarchies for each revoked item |
|
|
|
|
model = model_registry.get_model(model_name) |
|
|
|
|
for item in items: |
|
|
|
|
try: |
|
|
|
|
instance = model.objects.get(id=item['model_id']) |
|
|
|
|
self.add_parents_with_hierarchy_organizer(instance, revocations_parents_organizer) |
|
|
|
|
except model.DoesNotExist: |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
return revocations, revocations_parents_organizer |
|
|
|
|
|
|
|
|
|
def query_model_logs(self, last_update, user, device_id): |
|
|
|
|
log_query = Q(date__gt=last_update, user=user) |
|
|
|
|
if device_id: |
|
|
|
|
log_query &= ~Q(device_id=device_id) # exclude query |
|
|
|
|
return ModelLog.objects.filter(log_query) |
|
|
|
|
return ModelLog.objects.filter(log_query).order_by('date') |
|
|
|
|
|
|
|
|
|
class UserDataAccessApi(HierarchyApiView): |
|
|
|
|
permission_classes = [IsAuthenticated] |
|
|
|
|
|