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: user audit log #2266

Merged
merged 16 commits into from
Oct 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
40 changes: 32 additions & 8 deletions app/account_linking.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from app.db import Session
from app.email_utils import send_welcome_email
from app.partner_user_utils import create_partner_user, create_partner_subscription
from app.utils import sanitize_email, canonicalize_email
from app.errors import (
AccountAlreadyLinkedToAnotherPartnerException,
Expand All @@ -23,6 +24,7 @@
User,
Alias,
)
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.utils import random_string


Expand Down Expand Up @@ -66,9 +68,10 @@ def set_plan_for_partner_user(partner_user: PartnerUser, plan: SLPlan):
LOG.i(
f"Creating partner_subscription [user_id={partner_user.user_id}] [partner_id={partner_user.partner_id}]"
)
PartnerSubscription.create(
partner_user_id=partner_user.id,
end_at=plan.expiration,
create_partner_subscription(
partner_user=partner_user,
expiration=plan.expiration,
msg="Upgraded via partner. User did not have a previous partner subscription",
)
agent.record_custom_event("PlanChange", {"plan": "premium", "type": "new"})
else:
Expand All @@ -80,6 +83,11 @@ def set_plan_for_partner_user(partner_user: PartnerUser, plan: SLPlan):
"PlanChange", {"plan": "premium", "type": "extension"}
)
sub.end_at = plan.expiration
emit_user_audit_log(
user=partner_user.user,
action=UserAuditLogAction.SubscriptionExtended,
message="Extended partner subscription",
)
Session.commit()


Expand All @@ -98,8 +106,8 @@ def ensure_partner_user_exists_for_user(
if res and res.partner_id != partner.id:
raise AccountAlreadyLinkedToAnotherPartnerException()
if not res:
res = PartnerUser.create(
user_id=sl_user.id,
res = create_partner_user(
user=sl_user,
partner_id=partner.id,
partner_email=link_request.email,
external_user_id=link_request.external_user_id,
Expand Down Expand Up @@ -140,8 +148,8 @@ def process(self) -> LinkResult:
activated=True,
from_partner=self.link_request.from_partner,
)
partner_user = PartnerUser.create(
user_id=new_user.id,
partner_user = create_partner_user(
user=new_user,
partner_id=self.partner.id,
external_user_id=self.link_request.external_user_id,
partner_email=self.link_request.email,
Expand Down Expand Up @@ -200,7 +208,7 @@ def get_login_strategy(
return ExistingUnlinkedUserStrategy(link_request, user, partner)


def check_alias(email: str) -> bool:
def check_alias(email: str):
alias = Alias.get_by(email=email)
if alias is not None:
raise AccountIsUsingAliasAsEmail()
Expand Down Expand Up @@ -275,10 +283,26 @@ def switch_already_linked_user(
LOG.i(
f"Deleting previous partner_user:{other_partner_user.id} from user:{current_user.id}"
)

emit_user_audit_log(
user=other_partner_user.user,
action=UserAuditLogAction.UnlinkAccount,
message=f"Deleting partner_user {other_partner_user.id} (external_user_id={other_partner_user.external_user_id} | partner_email={other_partner_user.partner_email}) from user {current_user.id}, as we received a new link request for the same partner",
)
PartnerUser.delete(other_partner_user.id)
LOG.i(f"Linking partner_user:{partner_user.id} to user:{current_user.id}")
# Link this partner_user to the current user
emit_user_audit_log(
user=partner_user.user,
action=UserAuditLogAction.UnlinkAccount,
message=f"Unlinking from partner, as user will now be tied to another external account. old=(id={partner_user.user.id} | email={partner_user.user.email}) | new=(id={current_user.id} | email={current_user.email})",
)
partner_user.user_id = current_user.id
emit_user_audit_log(
user=current_user,
action=UserAuditLogAction.LinkAccount,
message=f"Linking user {current_user.id} ({current_user.email}) to partner_user:{partner_user.id} (external_user_id={partner_user.external_user_id} | partner_email={partner_user.partner_email})",
)
# Set plan
set_plan_for_partner_user(partner_user, link_request.plan)
Session.commit()
Expand Down
6 changes: 6 additions & 0 deletions app/api/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app.extensions import limiter
from app.log import LOG
from app.models import Job, ApiToCookieToken
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction


@api_bp.route("/user", methods=["DELETE"])
Expand All @@ -16,6 +17,11 @@ def delete_user():

"""
# Schedule delete account job
emit_user_audit_log(
user=g.user,
action=UserAuditLogAction.UserMarkedForDeletion,
message=f"Marked user {g.user.id} ({g.user.email}) for deletion from API",
)
LOG.w("schedule delete account job for %s", g.user)
Job.create(
name=config.JOB_DELETE_ACCOUNT,
Expand Down
7 changes: 7 additions & 0 deletions app/contact_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from app.email_validation import is_valid_email
from app.log import LOG
from app.models import Contact, Alias
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.utils import sanitize_email


Expand Down Expand Up @@ -100,6 +101,12 @@ def create_contact(
invalid_email=email == "",
commit=True,
)
emit_user_audit_log(
user=alias.user,
action=UserAuditLogAction.CreateContact,
message=f"Created contact {contact.id} ({contact.email})",
commit=True,
)
LOG.d(
f"Created contact {contact} for alias {alias} with email {email} invalid_email={contact.invalid_email}"
)
Expand Down
12 changes: 12 additions & 0 deletions app/custom_domain_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app.email_utils import get_email_domain_part
from app.log import LOG
from app.models import User, CustomDomain, SLDomain, Mailbox, Job, DomainMailbox
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction

_ALLOWED_DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$")
_MAX_MAILBOXES_PER_DOMAIN = 20
Expand Down Expand Up @@ -137,6 +138,11 @@ def create_custom_domain(
if partner_id is not None:
new_custom_domain.partner_id = partner_id

emit_user_audit_log(
user=user,
action=UserAuditLogAction.CreateCustomDomain,
message=f"Created custom domain {new_custom_domain.id} ({new_domain})",
)
Session.commit()

return CreateCustomDomainResult(
Expand Down Expand Up @@ -190,5 +196,11 @@ def set_custom_domain_mailboxes(
for mailbox in mailboxes:
DomainMailbox.create(domain_id=custom_domain.id, mailbox_id=mailbox.id)

mailboxes_as_str = ",".join(map(str, mailbox_ids))
emit_user_audit_log(
user=custom_domain.user,
action=UserAuditLogAction.UpdateCustomDomain,
message=f"Updated custom domain {custom_domain.id} mailboxes (domain={custom_domain.domain}) (mailboxes={mailboxes_as_str})",
)
Session.commit()
return SetCustomDomainMailboxesResult(success=True)
27 changes: 27 additions & 0 deletions app/custom_domain_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
get_network_dns_client,
)
from app.models import CustomDomain
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.utils import random_string


Expand Down Expand Up @@ -121,6 +122,12 @@ def validate_dkim_records(self, custom_domain: CustomDomain) -> dict[str, str]:
# Original DKIM record is not there, which means the DKIM config is not finished. Proceed with the
# rest of the code path, returning the invalid records and clearing the flag
custom_domain.dkim_verified = len(invalid_records) == 0
if custom_domain.dkim_verified:
emit_user_audit_log(
user=custom_domain.user,
action=UserAuditLogAction.VerifyCustomDomain,
message=f"Verified DKIM records for custom domain {custom_domain.id} ({custom_domain.domain})",
)
Session.commit()
return invalid_records

Expand All @@ -137,6 +144,11 @@ def validate_domain_ownership(

if expected_verification_record in txt_records:
custom_domain.ownership_verified = True
emit_user_audit_log(
user=custom_domain.user,
action=UserAuditLogAction.VerifyCustomDomain,
message=f"Verified ownership for custom domain {custom_domain.id} ({custom_domain.domain})",
)
Session.commit()
return DomainValidationResult(success=True, errors=[])
else:
Expand All @@ -155,6 +167,11 @@ def validate_mx_records(
)
else:
custom_domain.verified = True
emit_user_audit_log(
user=custom_domain.user,
action=UserAuditLogAction.VerifyCustomDomain,
message=f"Verified MX records for custom domain {custom_domain.id} ({custom_domain.domain})",
)
Session.commit()
return DomainValidationResult(success=True, errors=[])

Expand All @@ -165,6 +182,11 @@ def validate_spf_records(
expected_spf_domain = self.get_expected_spf_domain(custom_domain)
if expected_spf_domain in spf_domains:
custom_domain.spf_verified = True
emit_user_audit_log(
user=custom_domain.user,
action=UserAuditLogAction.VerifyCustomDomain,
message=f"Verified SPF records for custom domain {custom_domain.id} ({custom_domain.domain})",
)
Session.commit()
return DomainValidationResult(success=True, errors=[])
else:
Expand All @@ -183,6 +205,11 @@ def validate_dmarc_records(
txt_records = self._dns_client.get_txt_record("_dmarc." + custom_domain.domain)
if DMARC_RECORD in txt_records:
custom_domain.dmarc_verified = True
emit_user_audit_log(
user=custom_domain.user,
action=UserAuditLogAction.VerifyCustomDomain,
message=f"Verified DMARC records for custom domain {custom_domain.id} ({custom_domain.domain})",
)
Session.commit()
return DomainValidationResult(success=True, errors=[])
else:
Expand Down
9 changes: 8 additions & 1 deletion app/dashboard/views/alias_contact_manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from operator import or_
from typing import Optional

from flask import render_template, request, redirect, flash
from flask import url_for
Expand All @@ -22,6 +23,7 @@
)
from app.log import LOG
from app.models import Alias, Contact, EmailLog
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.utils import CSRFValidationForm


Expand Down Expand Up @@ -190,14 +192,19 @@ def get_contact_infos(


def delete_contact(alias: Alias, contact_id: int):
contact = Contact.get(contact_id)
contact: Optional[Contact] = Contact.get(contact_id)

if not contact:
flash("Unknown error. Refresh the page", "warning")
elif contact.alias_id != alias.id:
flash("You cannot delete reverse-alias", "warning")
else:
delete_contact_email = contact.website_email
emit_user_audit_log(
user=alias.user,
action=UserAuditLogAction.DeleteContact,
message=f"Delete contact {contact_id} ({contact.email})",
)
Contact.delete(contact_id)
Session.commit()

Expand Down
15 changes: 14 additions & 1 deletion app/dashboard/views/contact_detail.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional

from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
Expand All @@ -7,6 +9,7 @@
from app.db import Session
from app.models import Contact
from app.pgp_utils import PGPException, load_public_key_and_check
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction


class PGPContactForm(FlaskForm):
Expand All @@ -20,7 +23,7 @@ class PGPContactForm(FlaskForm):
@dashboard_bp.route("/contact/<int:contact_id>/", methods=["GET", "POST"])
@login_required
def contact_detail_route(contact_id):
contact = Contact.get(contact_id)
contact: Optional[Contact] = Contact.get(contact_id)
if not contact or contact.user_id != current_user.id:
flash("You cannot see this page", "warning")
return redirect(url_for("dashboard.index"))
Expand Down Expand Up @@ -50,6 +53,11 @@ def contact_detail_route(contact_id):
except PGPException:
flash("Cannot add the public key, please verify it", "error")
else:
emit_user_audit_log(
user=current_user,
action=UserAuditLogAction.UpdateContact,
message=f"Added PGP key {contact.pgp_public_key} for contact {contact_id} ({contact.email})",
)
Session.commit()
flash(
f"PGP public key for {contact.email} is saved successfully",
Expand All @@ -62,6 +70,11 @@ def contact_detail_route(contact_id):
)
elif pgp_form.action.data == "remove":
# Free user can decide to remove contact PGP key
emit_user_audit_log(
user=current_user,
action=UserAuditLogAction.UpdateContact,
message=f"Removed PGP key {contact.pgp_public_key} for contact {contact_id} ({contact.email})",
)
contact.pgp_public_key = None
contact.pgp_finger_print = None
Session.commit()
Expand Down
6 changes: 6 additions & 0 deletions app/dashboard/views/delete_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.dashboard.views.enter_sudo import sudo_required
from app.log import LOG
from app.models import Subscription, Job
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction


class DeleteDirForm(FlaskForm):
Expand All @@ -33,6 +34,11 @@ def delete_account():

# Schedule delete account job
LOG.w("schedule delete account job for %s", current_user)
emit_user_audit_log(
user=current_user,
action=UserAuditLogAction.UserMarkedForDeletion,
message=f"User {current_user.id} ({current_user.email}) marked for deletion via webapp",
)
Job.create(
name=JOB_DELETE_ACCOUNT,
payload={"user_id": current_user.id},
Expand Down
Loading
Loading