Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auditlog #231

Merged
merged 3 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 15 additions & 18 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

<hr>

### **✨ Build microservices as tenants on a Django monolith. Avoid the infra & operational overheads. Scale effortlessly. ✨**
<a href="https://www.zango.dev/blog/architecting-microservices-as-a-tenant-on-a-monolith" target="_blank">Know more</a>
### **✨ Build microservices as tenants on a Django monolith. Avoid the infra & operational overheads. Scale effortlessly. ✨**

<a href="https://www.zango.dev/blog/architecting-microservices-as-a-tenant-on-a-monolith" target="_blank">Know more</a>

<hr>
<p align="center">
<a href="#">
Expand All @@ -23,12 +25,9 @@
<a href="https://www.zango.dev/docs/category/getting-started" target="_blank">Getting Started </a>|
<a href="https://zango.dev/docs" target="_blank">Docs</a> |
<a href="https://discord.com/invite/WHvVjU23e7" target="_blank">Discord</a>
</p>



**Zango** is a web application development framework built upon Django, designed to host multiple apps or microservices as tenants on a single monolith under the hood.
</p>

**Zango** is a web application development framework built upon Django, designed to host multiple apps or microservices as tenants on a single monolith under the hood.

- Leverage the stengths of Django, an already proven and battle tested web framework
- Make available the basics of business web apps/ microservices as part of the framework
Expand All @@ -39,9 +38,9 @@

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Healthlane-Technologies/zelthy3-gitpod-sandbox-official/)


#### App Panel - Central hub to manage all your apps/ microservices
Perform tasks such as configuring permissions, managing user roles, and much more.

Perform tasks such as configuring permissions, managing user roles, and much more.

![Zango App Panel](https://github.com/Healthlane-Technologies/zango/assets/22682748/b593a821-ec1d-4082-a590-e5ed52cb0c28)

Expand All @@ -51,21 +50,21 @@ Zango redefines multi-tenancy by enabling multiple different apps to run on a si

![Zango Scaling](https://zelthy-initium-production-static.s3.amazonaws.com/static/zelthymain/react-images/cost-effective-scaling.svg)

#### 🚀 Getting Started:

#### 🚀 Getting Started:
- [Gitpod](https://www.zango.dev/docs/core/getting-started/installing-zelthy/gitpod)
- [Docker](https://www.zango.dev/docs/core/getting-started/installing-zelthy/docker)
- [Docker](https://www.zango.dev/docs/core/getting-started/installing-zelthy/docker)
- [Manual](https://www.zango.dev/docs/core/getting-started/installing-zelthy/manual)


#### 📦 Free Packages
A few essential packages are freely available. These packages enable development of a wide variety of applications and are available for installation from the App Panel.

A few essential packages are freely available. These packages enable development of a wide variety of applications and are available for installation from the App Panel.

- [Basic Auth](https://www.zango.dev/docs/login/introduction)
- [Frames](https://www.zango.dev/docs/frame/introduction)
- [CRUD](https://www.zango.dev/docs/crud/introduction)
- [CRUD](https://www.zango.dev/docs/crud/introduction)
- [Workflow](https://www.zango.dev/docs/workflow/overview)


#### 🌟 Get Involved and Make a Difference

Join our community and help build **Zango**. Here's how you can get involved:
Expand All @@ -78,6 +77,4 @@ Join our community and help build **Zango**. Here's how you can get involved:

Together, let's build something incredible! ✨🚀



#### Official Documentation: https://zango.dev/docs
#### Official Documentation: https://zango.dev/docs
Empty file.
Empty file.
63 changes: 63 additions & 0 deletions backend/src/zango/api/platform/auditlogs/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import importlib

from rest_framework import serializers

from zango.api.platform.tenancy.v1.serializers import AppUserModelSerializerModel
from zango.apps.auditlogs.models import LogEntry
from zango.core.utils import get_datetime_str_in_tenant_timezone, get_current_request


class AuditLogSerializerModel(serializers.ModelSerializer):
actor = serializers.SerializerMethodField()
actor_type = serializers.SerializerMethodField()
timestamp = serializers.SerializerMethodField()
action = serializers.SerializerMethodField()
object_uuid = serializers.SerializerMethodField()
object_type = serializers.SerializerMethodField()

def get_action(self, obj):
return obj.get_action_display().capitalize()

def get_timestamp(self, obj):
return get_datetime_str_in_tenant_timezone(
obj.timestamp, self.context["tenant"]
)

def get_actor(self, obj):
return (
obj.tenant_actor.name
if obj.tenant_actor
else obj.platform_actor.name
if obj.platform_actor
else None
)

def get_actor_type(self, obj):
return (
"tenant_actor"
if obj.tenant_actor
else "platform_actor"
if obj.platform_actor
else None
)

def get_object_uuid(self, obj):
if obj.object_ref is not None:
return str(obj.object_ref.object_uuid)

def get_object_type(self, obj):
return obj.content_type.model

class Meta:
model = LogEntry
fields = [
"id",
"actor",
"actor_type",
"action",
"object_id",
"object_uuid",
"object_type",
"timestamp",
"changes",
]
11 changes: 11 additions & 0 deletions backend/src/zango/api/platform/auditlogs/v1/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import path

from .views import AuditLogViewAPIV1

urlpatterns = [
path(
"",
AuditLogViewAPIV1.as_view(),
name="auditlog-apiv1-auditloglistview",
),
]
182 changes: 182 additions & 0 deletions backend/src/zango/api/platform/auditlogs/v1/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import traceback
import csv
from datetime import datetime
import json
import pytz

from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.utils.decorators import method_decorator

from zango.core.api import get_api_response, ZangoGenericPlatformAPIView
from zango.core.api.utils import ZangoAPIPagination
from zango.core.permissions import IsSuperAdminPlatformUser
from zango.core.utils import get_search_columns
from zango.apps.shared.tenancy.models import TenantModel
from zango.apps.auditlogs.models import LogEntry
from zango.core.common_utils import set_app_schema_path

from .serializers import AuditLogSerializerModel


@method_decorator(set_app_schema_path, name="dispatch")
class AuditLogViewAPIV1(ZangoGenericPlatformAPIView, ZangoAPIPagination):
pagination_class = ZangoAPIPagination

def process_timestamp(self, timestamp, timezone):
try:
ts = json.loads(timestamp)
tz = pytz.timezone(timezone)
ts["start"] = tz.localize(
datetime.strptime(ts["start"] + "-" + "00:00", "%Y-%m-%d-%H:%M"),
is_dst=None,
)
ts["end"] = tz.localize(
datetime.strptime(ts["end"] + "-" + "23:59", "%Y-%m-%d-%H:%M"),
is_dst=None,
)
return ts
except Exception:
return None

def process_id(self, id):
try:
return int(id)
except ValueError:
return None

def get_queryset(self, search, tenant, columns={}, model_type=None):
field_name_query_mapping = {
"tenant_actor": "tenant_actor__name__icontains",
"platform_actor": "platform_actor__name__icontains",
"object_id": "object_id",
"id": "id",
"object_repr": "object_repr__icontains",
"changes": "changes__icontains",
"object_uuid": "object_ref__object_uuid__icontains",
}
search_filters = {
"id": self.process_id,
"object_id": self.process_id,
"timestamp": self.process_timestamp,
}
if model_type == "dynamic_models":
records = (
LogEntry.objects.all()
.order_by("-id")
.filter(content_type__app_label=model_type)
)
elif model_type == "core_models":
records = (
LogEntry.objects.all()
.order_by("-id")
.exclude(content_type__app_label="dynamic_models")
)
else:
records = LogEntry.objects.all().order_by("-id")
if search == "" and columns == {}:
return records
filters = Q()
for field_name, query in field_name_query_mapping.items():
if search:
if search_filters.get(field_name, None):
filters |= Q(**{query: search_filters[field_name](search)})
else:
filters |= Q(**{query: search})
records = records.filter(filters).distinct()
if columns.get("timestamp"):
processed = self.process_timestamp(
columns.get("timestamp"), tenant.timezone
)
if processed is not None:
records = records.filter(
timestamp__gte=processed["start"], timestamp__lte=processed["end"]
)
if columns.get("action"):
records = records.filter(action=columns.get("action"))
if columns.get("object_type"):
records = records.filter(
content_type=ContentType.objects.get(id=columns.get("object_type"))
)
return records

def get_dropdown_options(self, model_type=None):
options = {}
options["action"] = [
{
"id": "0",
"label": "Create",
},
{
"id": "1",
"label": "Update",
},
{
"id": "2",
"label": "Delete",
},
]
options["object_type"] = []
if model_type == "dynamic_models":
object_types = list(
LogEntry.objects.all()
.values_list("content_type_id", "content_type__model")
.order_by("content_type__model")
.distinct()
.filter(content_type__app_label__contains="dynamic_models")
)
elif model_type == "core_models":
object_types = list(
LogEntry.objects.all()
.values_list("content_type_id", "content_type__model")
.order_by("content_type__model")
.distinct()
.exclude(content_type__app_label__contains="dynamic_models")
)
else:
object_types = list(
LogEntry.objects.all()
.values_list("content_type_id", "content_type__model")
.order_by("content_type__model")
.distinct()
)
for object_type in object_types:
options["object_type"].append(
{
"id": object_type[0],
"label": object_type[1],
}
)
return options

def get(self, request, *args, **kwargs):
try:
app_uuid = kwargs.get("app_uuid")
tenant = TenantModel.objects.get(uuid=app_uuid)
include_dropdown_options = request.GET.get("include_dropdown_options")
model_type = request.GET.get("model_type", None)
search = request.GET.get("search", None)
columns = get_search_columns(request)
audit_logs = self.get_queryset(search, tenant, columns, model_type)
paginated_audit_logs = self.paginate_queryset(
audit_logs, request, view=self
)
serializer = AuditLogSerializerModel(
paginated_audit_logs, many=True, context={"tenant": tenant}
)
auditlogs = self.get_paginated_response_data(serializer.data)
success = True
response = {
"audit_logs": auditlogs,
"message": "Audit logs fetched successfully",
}
if include_dropdown_options:
response["dropdown_options"] = self.get_dropdown_options(model_type)
status = 200
except Exception as e:
traceback.print_exc()
success = False
response = {"message": str(e)}
status = 500
return get_api_response(success, response, status)
2 changes: 2 additions & 0 deletions backend/src/zango/api/platform/tenancy/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from zango.api.platform.packages.v1 import urls as packages_v1_urls
from zango.api.platform.tasks.v1 import urls as tasks_v1_urls
from zango.api.platform.codeassist.v1 import urls as codeassist_v1_urls
from zango.api.platform.auditlogs.v1 import urls as auditlog_v1_urls


urlpatterns = [
Expand Down Expand Up @@ -57,5 +58,6 @@
re_path(r"^(?P<app_uuid>[\w-]+)/packages/$", include(packages_v1_urls)),
re_path(r"^(?P<app_uuid>[\w-]+)/tasks/", include(tasks_v1_urls)),
re_path(r"^(?P<app_uuid>[\w-]+)/code-assist/", include(codeassist_v1_urls)),
re_path(r"^(?P<app_uuid>[\w-]+)/auditlog/", include(auditlog_v1_urls)),
path("", include(permissions_v1_urls)),
]
1 change: 0 additions & 1 deletion backend/src/zango/api/platform/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from .tenancy.v1 import urls as tenancy_v1_urls
from .auth.v1 import urls as auth_v1_urls
from .packages.v1 import urls as packages_v1_urls

urlpatterns = [
path("v1/apps/", include(tenancy_v1_urls)),
Expand Down
10 changes: 8 additions & 2 deletions backend/src/zango/apps/appauth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
AbstractZangoUserModel,
AbstractOldPasswords,
)

from ..permissions.models import PolicyModel, PolicyGroupModel
from zango.apps.auditlogs.registry import auditlog

# from .perm_mixin import PolicyQsMixin
from ..permissions.models import PolicyModel, PolicyGroupModel
from ..permissions.mixin import PermissionMixin


Expand Down Expand Up @@ -264,3 +264,9 @@ def has_password_reset_step(self, request, days=90):

class OldPasswords(AbstractOldPasswords):
user = models.ForeignKey(AppUserModel, on_delete=models.PROTECT)


auditlog.register(AppUserModel, m2m_fields={"policies", "roles", "policy_groups"})
auditlog.register(OldPasswords)
auditlog.register(UserRoleModel, m2m_fields={"policy_groups", "policies"})

Loading