mirror of
https://github.com/simple-login/app.git
synced 2024-09-30 05:31:30 +02:00
Merge pull request #835 from acasajus/new/admin-audit-trail
New/admin audit trail
This commit is contained in:
commit
6d52daee21
@ -1,4 +1,7 @@
|
|||||||
import arrow
|
import arrow
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from app import models
|
||||||
from flask import redirect, url_for, request, flash
|
from flask import redirect, url_for, request, flash
|
||||||
from flask_admin import expose, AdminIndexView
|
from flask_admin import expose, AdminIndexView
|
||||||
from flask_admin.actions import action
|
from flask_admin.actions import action
|
||||||
@ -6,7 +9,15 @@ from flask_admin.contrib import sqla
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.models import User, ManualSubscription, Fido, Subscription, AppleSubscription
|
from app.models import (
|
||||||
|
User,
|
||||||
|
ManualSubscription,
|
||||||
|
Fido,
|
||||||
|
Subscription,
|
||||||
|
AppleSubscription,
|
||||||
|
AdminAuditLog,
|
||||||
|
AuditLogActionEnum,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SLModelView(sqla.ModelView):
|
class SLModelView(sqla.ModelView):
|
||||||
@ -25,6 +36,42 @@ class SLModelView(sqla.ModelView):
|
|||||||
# redirect to login page if user doesn't have access
|
# redirect to login page if user doesn't have access
|
||||||
return redirect(url_for("auth.login", next=request.url))
|
return redirect(url_for("auth.login", next=request.url))
|
||||||
|
|
||||||
|
def on_model_change(self, form, model, is_created):
|
||||||
|
changes = {}
|
||||||
|
for attr in sqlalchemy.inspect(model).attrs:
|
||||||
|
if attr.history.has_changes() and attr.key not in (
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
):
|
||||||
|
value = attr.value
|
||||||
|
# If it's a model reference, get the source id
|
||||||
|
if issubclass(type(value), models.Base):
|
||||||
|
value = value.id
|
||||||
|
# otherwise, if its a generic object stringify it
|
||||||
|
if issubclass(type(value), object):
|
||||||
|
value = str(value)
|
||||||
|
changes[attr.key] = value
|
||||||
|
auditAction = (
|
||||||
|
AuditLogActionEnum.create_object
|
||||||
|
if is_created
|
||||||
|
else AuditLogActionEnum.update_object
|
||||||
|
)
|
||||||
|
AdminAuditLog.create(
|
||||||
|
admin_user_id=current_user.id,
|
||||||
|
model=model.__class__.__name__,
|
||||||
|
model_id=model.id,
|
||||||
|
action=auditAction.value,
|
||||||
|
data=changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_model_delete(self, model):
|
||||||
|
AdminAuditLog.create(
|
||||||
|
admin_user_id=current_user.id,
|
||||||
|
model=model.__class__.__name__,
|
||||||
|
model_id=model.id,
|
||||||
|
action=AuditLogActionEnum.delete_object.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SLAdminIndexView(AdminIndexView):
|
class SLAdminIndexView(AdminIndexView):
|
||||||
@expose("/")
|
@expose("/")
|
||||||
@ -113,6 +160,9 @@ class UserAdmin(SLModelView):
|
|||||||
user.trial_end = arrow.now().shift(weeks=1)
|
user.trial_end = arrow.now().shift(weeks=1)
|
||||||
|
|
||||||
flash(f"Extend trial for {user} to {user.trial_end}", "success")
|
flash(f"Extend trial for {user} to {user.trial_end}", "success")
|
||||||
|
AdminAuditLog.extend_trial(
|
||||||
|
current_user.id, user.id, user.trial_end, "1 week"
|
||||||
|
)
|
||||||
|
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
@ -123,14 +173,19 @@ class UserAdmin(SLModelView):
|
|||||||
)
|
)
|
||||||
def disable_otp_fido(self, ids):
|
def disable_otp_fido(self, ids):
|
||||||
for user in User.filter(User.id.in_(ids)):
|
for user in User.filter(User.id.in_(ids)):
|
||||||
|
user_had_otp = user.enable_otp
|
||||||
if user.enable_otp:
|
if user.enable_otp:
|
||||||
user.enable_otp = False
|
user.enable_otp = False
|
||||||
flash(f"Disable OTP for {user}", "info")
|
flash(f"Disable OTP for {user}", "info")
|
||||||
|
|
||||||
|
user_had_fido = user.fido_uuid is not None
|
||||||
if user.fido_uuid:
|
if user.fido_uuid:
|
||||||
Fido.filter_by(uuid=user.fido_uuid).delete()
|
Fido.filter_by(uuid=user.fido_uuid).delete()
|
||||||
user.fido_uuid = None
|
user.fido_uuid = None
|
||||||
flash(f"Disable FIDO for {user}", "info")
|
flash(f"Disable FIDO for {user}", "info")
|
||||||
|
AdminAuditLog.disable_otp_fido(
|
||||||
|
current_user.id, user.id, user_had_otp, user_had_fido
|
||||||
|
)
|
||||||
|
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
@ -145,6 +200,7 @@ class UserAdmin(SLModelView):
|
|||||||
# return
|
# return
|
||||||
#
|
#
|
||||||
# for user in User.filter(User.id.in_(ids)):
|
# for user in User.filter(User.id.in_(ids)):
|
||||||
|
# AdminAuditLog.logged_as_user(current_user.id, user.id)
|
||||||
# login_user(user)
|
# login_user(user)
|
||||||
# flash(f"Login as user {user}", "success")
|
# flash(f"Login as user {user}", "success")
|
||||||
# return redirect("/")
|
# return redirect("/")
|
||||||
@ -172,6 +228,7 @@ def manual_upgrade(way: str, ids: [int], is_giveaway: bool):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
AdminAuditLog.create_manual_upgrade(current_user.id, way, user.id, is_giveaway)
|
||||||
manual_sub: ManualSubscription = ManualSubscription.get_by(user_id=user.id)
|
manual_sub: ManualSubscription = ManualSubscription.get_by(user_id=user.id)
|
||||||
if manual_sub:
|
if manual_sub:
|
||||||
# renew existing subscription
|
# renew existing subscription
|
||||||
@ -179,7 +236,6 @@ def manual_upgrade(way: str, ids: [int], is_giveaway: bool):
|
|||||||
manual_sub.end_at = manual_sub.end_at.shift(years=1)
|
manual_sub.end_at = manual_sub.end_at.shift(years=1)
|
||||||
else:
|
else:
|
||||||
manual_sub.end_at = arrow.now().shift(years=1, days=1)
|
manual_sub.end_at = arrow.now().shift(years=1, days=1)
|
||||||
Session.commit()
|
|
||||||
flash(f"Subscription extended to {manual_sub.end_at.humanize()}", "success")
|
flash(f"Subscription extended to {manual_sub.end_at.humanize()}", "success")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -188,10 +244,10 @@ def manual_upgrade(way: str, ids: [int], is_giveaway: bool):
|
|||||||
end_at=arrow.now().shift(years=1, days=1),
|
end_at=arrow.now().shift(years=1, days=1),
|
||||||
comment=way,
|
comment=way,
|
||||||
is_giveaway=is_giveaway,
|
is_giveaway=is_giveaway,
|
||||||
commit=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
flash(f"New {way} manual subscription for {user} is created", "success")
|
flash(f"New {way} manual subscription for {user} is created", "success")
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
|
|
||||||
class EmailLogAdmin(SLModelView):
|
class EmailLogAdmin(SLModelView):
|
||||||
@ -235,6 +291,9 @@ class ManualSubscriptionAdmin(SLModelView):
|
|||||||
for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)):
|
for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)):
|
||||||
ms.end_at = ms.end_at.shift(years=1)
|
ms.end_at = ms.end_at.shift(years=1)
|
||||||
flash(f"Extend subscription for 1 year for {ms.user}", "success")
|
flash(f"Extend subscription for 1 year for {ms.user}", "success")
|
||||||
|
AdminAuditLog.extend_subscription(
|
||||||
|
current_user.id, ms.user.id, ms.end_at, "1 year"
|
||||||
|
)
|
||||||
|
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
@ -247,6 +306,9 @@ class ManualSubscriptionAdmin(SLModelView):
|
|||||||
for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)):
|
for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)):
|
||||||
ms.end_at = ms.end_at.shift(months=1)
|
ms.end_at = ms.end_at.shift(months=1)
|
||||||
flash(f"Extend subscription for 1 month for {ms.user}", "success")
|
flash(f"Extend subscription for 1 month for {ms.user}", "success")
|
||||||
|
AdminAuditLog.extend_subscription(
|
||||||
|
current_user.id, ms.user.id, ms.end_at, "1 month"
|
||||||
|
)
|
||||||
|
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
@ -280,3 +342,27 @@ class ReferralAdmin(SLModelView):
|
|||||||
# can_edit = True
|
# can_edit = True
|
||||||
# can_create = True
|
# can_create = True
|
||||||
# can_delete = True
|
# can_delete = True
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_action_formatter(view, context, model, name):
|
||||||
|
action_name = AuditLogActionEnum.get_name(model.action)
|
||||||
|
return "{} ({})".format(action_name, model.action)
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_created_at_formatter(view, context, model, name):
|
||||||
|
return model.created_at.format()
|
||||||
|
|
||||||
|
|
||||||
|
class AdminAuditLogAdmin(SLModelView):
|
||||||
|
column_searchable_list = ["admin.id", "admin.email", "model_id", "created_at"]
|
||||||
|
column_filters = ["admin.id", "admin.email", "model_id", "created_at"]
|
||||||
|
column_exclude_list = ["id"]
|
||||||
|
column_hide_backrefs = False
|
||||||
|
can_edit = False
|
||||||
|
can_create = False
|
||||||
|
can_delete = False
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"action": _admin_action_formatter,
|
||||||
|
"created_at": _admin_created_at_formatter,
|
||||||
|
}
|
||||||
|
102
app/models.py
102
app/models.py
@ -226,6 +226,17 @@ class BlockBehaviourEnum(EnumE):
|
|||||||
return_5xx = 1
|
return_5xx = 1
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLogActionEnum(EnumE):
|
||||||
|
create_object = 0
|
||||||
|
update_object = 1
|
||||||
|
delete_object = 2
|
||||||
|
manual_upgrade = 3
|
||||||
|
extend_trial = 4
|
||||||
|
disable_2fa = 5
|
||||||
|
logged_as_user = 6
|
||||||
|
extend_subscription = 7
|
||||||
|
|
||||||
|
|
||||||
class Hibp(Base, ModelMixin):
|
class Hibp(Base, ModelMixin):
|
||||||
__tablename__ = "hibp"
|
__tablename__ = "hibp"
|
||||||
name = sa.Column(sa.String(), nullable=False, unique=True, index=True)
|
name = sa.Column(sa.String(), nullable=False, unique=True, index=True)
|
||||||
@ -2878,4 +2889,95 @@ class PhoneMessage(Base, ModelMixin):
|
|||||||
number = orm.relationship(PhoneNumber)
|
number = orm.relationship(PhoneNumber)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminAuditLog(Base):
|
||||||
|
__tablename__ = "admin_audit_log"
|
||||||
|
|
||||||
|
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
|
created_at = sa.Column(ArrowType, default=arrow.utcnow, nullable=False)
|
||||||
|
admin_user_id = sa.Column(sa.ForeignKey("users.id"), nullable=False)
|
||||||
|
action = sa.Column(sa.Integer, nullable=False)
|
||||||
|
model = sa.Column(sa.Text, nullable=False)
|
||||||
|
model_id = sa.Column(sa.Integer, nullable=True)
|
||||||
|
data = sa.Column(sa.JSON, nullable=True)
|
||||||
|
|
||||||
|
admin = orm.relationship(User, foreign_keys=[admin_user_id])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, **kw):
|
||||||
|
r = cls(**kw)
|
||||||
|
Session.add(r)
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_manual_upgrade(
|
||||||
|
cls, admin_user_id: int, upgrade_type: str, user_id: int, giveaway: bool
|
||||||
|
):
|
||||||
|
cls.create(
|
||||||
|
admin_user_id=admin_user_id,
|
||||||
|
action=AuditLogActionEnum.manual_upgrade.value,
|
||||||
|
model="User",
|
||||||
|
model_id=user_id,
|
||||||
|
data={
|
||||||
|
"upgrade_type": upgrade_type,
|
||||||
|
"giveaway": giveaway,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def extend_trial(
|
||||||
|
cls, admin_user_id: int, user_id: int, trial_end: arrow.Arrow, extend_time: str
|
||||||
|
):
|
||||||
|
cls.create(
|
||||||
|
admin_user_id=admin_user_id,
|
||||||
|
action=AuditLogActionEnum.extend_trial.value,
|
||||||
|
model="User",
|
||||||
|
model_id=user_id,
|
||||||
|
data={
|
||||||
|
"trial_end": trial_end.format(arrow.FORMAT_RFC3339),
|
||||||
|
"extend_time": extend_time,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def disable_otp_fido(
|
||||||
|
cls, admin_user_id: int, user_id: int, had_otp: bool, had_fido: bool
|
||||||
|
):
|
||||||
|
cls.create(
|
||||||
|
admin_user_id=admin_user_id,
|
||||||
|
action=AuditLogActionEnum.disable_2fa.value,
|
||||||
|
model="User",
|
||||||
|
model_id=user_id,
|
||||||
|
data={"had_otp": had_otp, "had_fido": had_fido},
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def logged_as_user(cls, admin_user_id: int, user_id: int):
|
||||||
|
cls.create(
|
||||||
|
admin_user_id=admin_user_id,
|
||||||
|
action=AuditLogActionEnum.logged_as_user.value,
|
||||||
|
model="User",
|
||||||
|
model_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def extend_subscription(
|
||||||
|
cls,
|
||||||
|
admin_user_id: int,
|
||||||
|
user_id: int,
|
||||||
|
subscription_end: arrow.Arrow,
|
||||||
|
extend_time: str,
|
||||||
|
):
|
||||||
|
cls.create(
|
||||||
|
admin_user_id=admin_user_id,
|
||||||
|
action=AuditLogActionEnum.extend_subscription.value,
|
||||||
|
model="User",
|
||||||
|
model_id=user_id,
|
||||||
|
data={
|
||||||
|
"subscription_end": subscription_end.format(arrow.FORMAT_RFC3339),
|
||||||
|
"extend_time": extend_time,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
"""Create admin audit log
|
||||||
|
|
||||||
|
Revision ID: b500363567e3
|
||||||
|
Revises: 9282e982bc05
|
||||||
|
Create Date: 2022-03-10 15:26:54.538717
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "b500363567e3"
|
||||||
|
down_revision = "4729b7096d12"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
admin_table = op.create_table(
|
||||||
|
"admin_audit_log",
|
||||||
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column("created_at", sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
|
||||||
|
sa.Column("admin_user_id", sa.Integer, nullable=False),
|
||||||
|
sa.Column("action", sa.Integer, nullable=False),
|
||||||
|
sa.Column("model", sa.String(length=256), nullable=False),
|
||||||
|
sa.Column("model_id", sa.Integer, nullable=True),
|
||||||
|
sa.Column("data", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||||
|
sa.Index("admin_audit_log_admin_user_id_idx", 'admin_user_id'),
|
||||||
|
sa.ForeignKeyConstraint(['admin_user_id'], ['users.id'], ondelete='cascade'),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table("admin_audit_log")
|
@ -36,6 +36,7 @@ from app.admin_model import (
|
|||||||
ManualSubscriptionAdmin,
|
ManualSubscriptionAdmin,
|
||||||
CouponAdmin,
|
CouponAdmin,
|
||||||
CustomDomainAdmin,
|
CustomDomainAdmin,
|
||||||
|
AdminAuditLogAdmin,
|
||||||
)
|
)
|
||||||
from app.api.base import api_bp
|
from app.api.base import api_bp
|
||||||
from app.auth.base import auth_bp
|
from app.auth.base import auth_bp
|
||||||
@ -88,6 +89,7 @@ from app.models import (
|
|||||||
Contact,
|
Contact,
|
||||||
ManualSubscription,
|
ManualSubscription,
|
||||||
Coupon,
|
Coupon,
|
||||||
|
AdminAuditLog,
|
||||||
)
|
)
|
||||||
from app.monitor.base import monitor_bp
|
from app.monitor.base import monitor_bp
|
||||||
from app.oauth.base import oauth_bp
|
from app.oauth.base import oauth_bp
|
||||||
@ -688,6 +690,7 @@ def init_admin(app):
|
|||||||
admin.add_view(CouponAdmin(Coupon, Session))
|
admin.add_view(CouponAdmin(Coupon, Session))
|
||||||
admin.add_view(ManualSubscriptionAdmin(ManualSubscription, Session))
|
admin.add_view(ManualSubscriptionAdmin(ManualSubscription, Session))
|
||||||
admin.add_view(CustomDomainAdmin(CustomDomain, Session))
|
admin.add_view(CustomDomainAdmin(CustomDomain, Session))
|
||||||
|
admin.add_view(AdminAuditLogAdmin(AdminAuditLog, Session))
|
||||||
|
|
||||||
|
|
||||||
def register_custom_commands(app):
|
def register_custom_commands(app):
|
||||||
|
Loading…
Reference in New Issue
Block a user