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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: audit additional events #2918

Open
wants to merge 87 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
509d68a
Add organisation to AuditLog, nullable for now
riceyrice Oct 18, 2023
42340d9
Fix BaseAppConfig default confusion
riceyrice Oct 18, 2023
38185a9
Audit project create/delete
riceyrice Oct 18, 2023
d96ec28
little bit of type checking
riceyrice Oct 18, 2023
0031f38
align backend/frontend types
riceyrice Oct 18, 2023
0908759
Missed constants for audit project create/delete
riceyrice Oct 18, 2023
a5f9ec4
Audit user/group - first pass
riceyrice Oct 18, 2023
7b2dcfa
remove migrations
riceyrice Oct 18, 2023
535e522
Upgrade django-simple-history
riceyrice Oct 18, 2023
95ffe0c
Patch django-simple-history
riceyrice Oct 18, 2023
c682009
add m2m fields and restore migrations
riceyrice Oct 18, 2023
57db8c3
derive history_record_class_path
riceyrice Oct 18, 2023
cd57d88
refactor
riceyrice Oct 19, 2023
e961cc6
Audit permissions first pass
riceyrice Oct 19, 2023
26d1988
login/logout first pass
riceyrice Oct 19, 2023
6029ab9
For non-validation errors, codes is a str
riceyrice Oct 25, 2023
25928c9
ModelBackend consistency
riceyrice Oct 25, 2023
6a0c085
Refactor pre/post authenticate checks
riceyrice Oct 25, 2023
4318ead
Remove redundant backend
riceyrice Oct 25, 2023
a453dde
Add rbac types
riceyrice Oct 25, 2023
e117aa7
linting
riceyrice Oct 27, 2023
d2c97f0
track m2m add/remove
riceyrice Oct 27, 2023
3b65f9a
organisations may be []
riceyrice Oct 27, 2023
3128374
audit user org add/remove
riceyrice Oct 27, 2023
087deaf
audit MFA methods
riceyrice Oct 31, 2023
3ed7a8a
audit mfa methods - second pass
riceyrice Oct 31, 2023
10c4e6e
refactor
riceyrice Oct 31, 2023
3942b02
simplify permissions
riceyrice Nov 1, 2023
676fe5f
fix simple history create m2m record fails
riceyrice Nov 1, 2023
f73a368
fix simple history m2m ignores settings
riceyrice Nov 1, 2023
ad36df0
fix simple history m2m misses direct save/delete
riceyrice Nov 1, 2023
8b87cf9
log changes to through models
riceyrice Nov 1, 2023
60f4fc2
fix organisation-related changes leak/lost
riceyrice Nov 2, 2023
71dda3d
do not forward delta for another model
riceyrice Nov 2, 2023
d99e81e
unit test TODOs
riceyrice Nov 2, 2023
b379f16
more TODOs
riceyrice Nov 2, 2023
bd14de5
TODOs
riceyrice Nov 2, 2023
78d63be
linting
riceyrice Nov 2, 2023
e88c138
audit IP address
riceyrice Nov 2, 2023
78132c6
CR changes
riceyrice Nov 7, 2023
8feb7df
Use mixins
riceyrice Nov 7, 2023
1216c72
CR: make getters public
riceyrice Nov 16, 2023
86dc63f
CR change: simplify auditable base class
riceyrice Nov 16, 2023
dab4d45
comment to explain unexpected self
riceyrice Nov 16, 2023
65b5f03
try to fix loose migration dependency
riceyrice Nov 16, 2023
c11bf3e
fix some tests
riceyrice Nov 17, 2023
f5b37c1
avoid m2m sadness
riceyrice Nov 17, 2023
16d800f
fix test
riceyrice Nov 17, 2023
12a42dc
more m2m sadness
riceyrice Nov 22, 2023
cbe7d88
missed m2m sadness
riceyrice Nov 23, 2023
2402fc9
avoid weird patch race condition
riceyrice Nov 23, 2023
c422925
count new audit logs
riceyrice Nov 23, 2023
2055dc1
fix excess queries
riceyrice Nov 23, 2023
6be9e9e
duh
riceyrice Nov 23, 2023
c3b8198
linting
riceyrice Nov 23, 2023
8c2824b
fix third-party model audit
riceyrice Nov 23, 2023
cdb7c0a
return token with up-to-date user
riceyrice Nov 24, 2023
a5fe7f6
fix last_login confusion
riceyrice Nov 24, 2023
14c61d2
update custom_auth test in line with saml test
riceyrice Nov 28, 2023
3d9e8a3
test user-org add/update, user-group add/remove
riceyrice Nov 28, 2023
11db98c
test user-group update
riceyrice Nov 28, 2023
1617265
test org add/remove
riceyrice Nov 30, 2023
e8fbb87
project/permission create/update/delete
riceyrice Nov 30, 2023
4c10bb2
more org/proj/env/perm tests
riceyrice Nov 30, 2023
8d86e85
user/group/org tests
riceyrice Dec 1, 2023
00aaf68
test oauth login/denied
riceyrice Dec 1, 2023
5132719
fix test
riceyrice Dec 1, 2023
32741d6
test MFA/login/logout
riceyrice Dec 6, 2023
106fdd0
TODONE
riceyrice Dec 6, 2023
a9e938c
avoid throttle settings clash
riceyrice Dec 7, 2023
d38db66
clearer grant logging
riceyrice Dec 7, 2023
314c148
fix user self-delete
riceyrice Dec 13, 2023
bfb93bf
accidentally picked these up in rebase
riceyrice Dec 13, 2023
964f686
fix rebase mistakes maybe
riceyrice Dec 13, 2023
edfff53
fix lock file mangled by rebase maybe
riceyrice Dec 13, 2023
ca1cd69
post-rebase bump migrations/models
riceyrice Dec 14, 2023
f91dc07
fix 26 tests
riceyrice Dec 14, 2023
c4d0ba2
fix more tests
riceyrice Dec 15, 2023
98051dc
Merge branch 'main' into feat/2797/audit-additional-events
riceyrice Dec 19, 2023
f0b6aac
upgrade history for recently-added model
riceyrice Dec 19, 2023
5c56ef6
Merge branch 'main' into feat/2797/audit-additional-events
matthewelwell Dec 20, 2023
3dd0f44
Merge branch 'main' into feat/2797/audit-additional-events
matthewelwell Dec 20, 2023
0d15a4d
Merge branch 'main' into feat/2797/audit-additional-events
matthewelwell Dec 22, 2023
06a5137
Merge branch 'main' into feat/2797/audit-additional-events
matthewelwell Jan 3, 2024
2633259
Merge branch 'main' into feat/2797/audit-additional-events
matthewelwell Jan 10, 2024
5a925ac
Use kwarg
matthewelwell Jan 10, 2024
41c2163
Merge branch 'main' into feat/2797/audit-additional-events
matthewelwell Apr 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,6 @@
AUTH_CONTROLLER_INSTALLED = importlib.util.find_spec("auth_controller") is not None
if AUTH_CONTROLLER_INSTALLED:
INSTALLED_APPS.append("auth_controller")
AUTHENTICATION_BACKENDS.insert(0, "auth_controller.backends.AuthControllerBackend")

IS_RBAC_INSTALLED = importlib.util.find_spec("rbac") is not None
if IS_RBAC_INSTALLED:
Expand Down
1 change: 0 additions & 1 deletion api/audit/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
default_app_config = "audit.apps.AuditConfig"
3 changes: 1 addition & 2 deletions api/audit/apps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from __future__ import unicode_literals

from django.apps import AppConfig


class AuditConfig(AppConfig):
default = True
name = "audit"

def ready(self):
Expand Down
8 changes: 8 additions & 0 deletions api/audit/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# generic/default audit messages

CREATED_MESSAGE = "New {model_name} created: {identity}"
DELETED_MESSAGE = "{model_name} deleted: {identity}"
UPDATED_MESSAGE = "{model_name} {field} {action}: {identity}"

# specific audit messages

FEATURE_CREATED_MESSAGE = "New Flag / Remote Config created: %s"
FEATURE_DELETED_MESSAGE = "Flag / Remote Config Deleted: %s"
FEATURE_UPDATED_MESSAGE = "Flag / Remote Config updated: %s"
Expand Down
20 changes: 20 additions & 0 deletions api/audit/migrations/0014_auditlog__organisation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.23 on 2023-12-14 15:15

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('organisations', '0049_subscription_billing_status'),
('audit', '0013_allow_manual_override_of_created_date'),
]

operations = [
migrations.AddField(
model_name='auditlog',
name='_organisation',
field=models.ForeignKey(db_column='organisation', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='audit_logs', to='organisations.organisation'),
),
]
18 changes: 18 additions & 0 deletions api/audit/migrations/0015_auditlog_ip_address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2023-12-14 15:15

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('audit', '0014_auditlog__organisation'),
]

operations = [
migrations.AddField(
model_name='auditlog',
name='ip_address',
field=models.GenericIPAddressField(blank=True, null=True),
),
]
49 changes: 46 additions & 3 deletions api/audit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from api_keys.models import MasterAPIKey
from audit.related_object_type import RelatedObjectType
from organisations.models import Organisation
from projects.models import Project

RELATED_OBJECT_TYPES = ((tag.name, tag.value) for tag in RelatedObjectType)
Expand All @@ -23,15 +24,26 @@
class AuditLog(LifecycleModel):
created_date = models.DateTimeField("DateCreated")

# TODO #2797: data migrate missing organisation values and rename this
_organisation = models.ForeignKey(
Organisation,
db_column="organisation",
related_name="audit_logs",
null=True,
on_delete=models.DO_NOTHING,
)
_organisation_id: int | None
project = models.ForeignKey(
Project, related_name="audit_logs", null=True, on_delete=models.DO_NOTHING
)
project_id: int | None
environment = models.ForeignKey(
"environments.Environment",
related_name="audit_logs",
null=True,
on_delete=models.DO_NOTHING,
)
environment_id: int | None

log = models.TextField()
author = models.ForeignKey(
Expand All @@ -41,6 +53,8 @@ class AuditLog(LifecycleModel):
blank=True,
on_delete=models.SET_NULL,
)
author_id: int | None
ip_address = models.GenericIPAddressField(null=True, blank=True)
master_api_key = models.ForeignKey(
MasterAPIKey,
related_name="audit_logs",
Expand Down Expand Up @@ -68,6 +82,28 @@ class Meta:
verbose_name_plural = "Audit Logs"
ordering = ("-created_date",)

# TODO #2797: data migrate missing organisation values and remove these

@property
def organisation(self) -> Organisation | None:
return self._organisation or (
self.project.organisation if self.project else None
)

@organisation.setter
def organisation(self, value):
self._organisation = value

@property
def organisation_id(self) -> int | None:
return self._organisation_id or (
self.project.organisation_id if self.project else None
)

@organisation_id.setter
def organisation_id(self, value):
self._organisation_id = value

@property
def environment_document_updated(self) -> bool:
if self.related_object_type == RelatedObjectType.CHANGE_REQUEST.name:
Expand Down Expand Up @@ -107,9 +143,12 @@ def get_history_record_model_class(
return getattr(module, class_name)

@hook(BEFORE_CREATE)
def add_project(self):
if self.environment and self.project is None:
self.project = self.environment.project
def add_project_organisation(self):
"""Ensure dependent fields are present"""
if self.project_id is None and self.environment:
self.project_id = self.environment.project_id
if self.organisation_id is None and self.project:
self.organisation_id = self.project.organisation_id
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved

@hook(BEFORE_CREATE)
def add_created_date(self) -> None:
Expand All @@ -126,6 +165,10 @@ def process_environment_update(self):
from environments.models import Environment
from environments.tasks import process_environment_update

# Some logs do not relate to projects/environments
if not self.project:
return

environments_filter = Q()
if self.environment_id:
environments_filter = Q(id=self.environment_id)
Expand Down
14 changes: 10 additions & 4 deletions api/audit/related_object_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@


class RelatedObjectType(enum.Enum):
CHANGE_REQUEST = "Change request"
EDGE_IDENTITY = "Edge identity"
ENVIRONMENT = "Environment"
FEATURE = "Feature"
FEATURE_STATE = "Feature state"
SEGMENT = "Segment"
ENVIRONMENT = "Environment"
CHANGE_REQUEST = "Change request"
EDGE_IDENTITY = "Edge Identity"
GRANT = "Grant"
GROUP = "Group"
IMPORT_REQUEST = "Import request"
PROJECT = "Project"
ROLE = "Role"
SEGMENT = "Segment"
USER = "User"
USER_MFA_METHOD = "User MFA method"
4 changes: 4 additions & 0 deletions api/audit/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

from audit.models import AuditLog
from environments.serializers import EnvironmentSerializerLight
from organisations.serializers import OrganisationSerializerBasic
from projects.serializers import ProjectListSerializer
from users.serializers import UserListSerializer


class AuditLogListSerializer(serializers.ModelSerializer):
author = UserListSerializer()
organisation = OrganisationSerializerBasic()
environment = EnvironmentSerializerLight()
project = ProjectListSerializer()

Expand All @@ -21,6 +23,7 @@ class Meta:
"created_date",
"log",
"author",
"organisation",
"environment",
"project",
"related_object_id",
Expand Down Expand Up @@ -83,6 +86,7 @@ def get_change_type(self, instance: AuditLog) -> str:


class AuditLogsQueryParamSerializer(serializers.Serializer):
organisation = serializers.IntegerField(required=False)
project = serializers.IntegerField(required=False)
environments = serializers.ListField(
child=serializers.IntegerField(min_value=0), required=False
Expand Down
16 changes: 5 additions & 11 deletions api/audit/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,20 @@


@receiver(post_save, sender=AuditLog)
def call_webhooks(sender, instance, **kwargs):
def call_webhooks(sender, instance: AuditLog, **kwargs):
if settings.DISABLE_WEBHOOKS:
return

if not (instance.project_id or instance.environment_id):
logger.warning("Audit log without project or environment. Not sending webhook.")
if not (organisation := instance.organisation):
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved
logger.warning("Audit log without organisation. Not sending webhook.")
return

organisation_id = (
instance.project.organisation_id
if instance.project
else instance.environment.project.organisation_id
)

if OrganisationWebhook.objects.filter(
organisation_id=organisation_id, enabled=True
organisation=organisation, enabled=True
).exists():
data = AuditLogListSerializer(instance=instance).data
call_organisation_webhooks.delay(
args=(organisation_id, data, WebhookEventType.AUDIT_LOG_CREATED.value)
args=(organisation.id, data, WebhookEventType.AUDIT_LOG_CREATED.value)
)


Expand Down