Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b18e3e2
Add initial audit_log table schema.
emmiegit Oct 7, 2025
8e6d6a1
Delete removed user_bot_owner table.
emmiegit Oct 7, 2025
72502cd
Add ip_address to audit_log schema.
emmiegit Oct 7, 2025
8db9492
Add two untyped ID columns to schema.
emmiegit Oct 7, 2025
88857d1
Add initial audit-log documentation file.
emmiegit Oct 7, 2025
7d9a778
Note event type format.
emmiegit Oct 7, 2025
1f2dc6c
Initialize AuditService.
emmiegit Oct 7, 2025
69f97c1
Add audit_log model to source.
emmiegit Oct 7, 2025
0e43334
Add initial AuditService::log() implementation.
emmiegit Oct 7, 2025
2046cc5
Fix rustdoc mention for macros.
emmiegit Oct 7, 2025
64cc959
Add initial audit! macro.
emmiegit Oct 7, 2025
a57319b
Extract optional revision ID for audit use later.
emmiegit Oct 7, 2025
cf0609e
Use last_insert_id in AuditService::log().
emmiegit Oct 7, 2025
391fa21
Add audit! for site.create
emmiegit Oct 7, 2025
e52f594
Add audit! for user.create
emmiegit Oct 7, 2025
9118fc4
Add audit! for page.create
emmiegit Oct 7, 2025
35fe0d8
Add audit! for page.edit
emmiegit Oct 7, 2025
ba805b3
Add ip_address to audit! calls.
emmiegit Oct 7, 2025
c0c8d4b
Replace audit! macros with AuditService calls.
emmiegit Oct 7, 2025
8ee4cc6
Add extra string columns to audit_log schema.
emmiegit Oct 8, 2025
8cbf386
Add audit event 'page.move'
emmiegit Oct 8, 2025
dad8956
Add audit event 'page.delete'
emmiegit Oct 8, 2025
eed11c5
Add audit event 'page.undelete'
emmiegit Oct 8, 2025
4cff03c
Move ip_address out of enum fields.
emmiegit Oct 8, 2025
d49d37b
Add audit event 'page.rollback'
emmiegit Oct 8, 2025
da2f9f6
Reword audit log notes.
emmiegit Oct 8, 2025
0ef76d3
Add stubbed page.undo
emmiegit Oct 8, 2025
7a7bb6c
Add audit log comment.
emmiegit Oct 8, 2025
24dbabb
Increase event_type width.
emmiegit Oct 8, 2025
a6733b3
Add audit event 'page_layout.update'
emmiegit Oct 8, 2025
4ab903f
Remove extra newline.
emmiegit Oct 8, 2025
abf7b9c
Add initial structures for audit event 'user.update'
emmiegit Oct 8, 2025
fdcd2f7
Move filter validation comment.
emmiegit Oct 8, 2025
546669c
Add constant for the disabled password hash.
emmiegit Oct 8, 2025
ffad785
Add audit event 'user.update'
emmiegit Oct 8, 2025
4f6ca83
Change order of variables.
emmiegit Oct 8, 2025
14b6657
Add audit event 'site.update'
emmiegit Oct 9, 2025
70248dc
Add audit event 'user.update_mfa'
emmiegit Oct 9, 2025
55c4e49
Create SessionService::get_user_with_id() helper method.
emmiegit Oct 9, 2025
5f1695e
Address clippy lint.
emmiegit Oct 9, 2025
4ff4422
Fix serialization of changed_fields types.
emmiegit Oct 9, 2025
df2167b
Pass in IP address to all events which need it.
emmiegit Oct 9, 2025
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
34 changes: 33 additions & 1 deletion deepwell/migrations/20220906103252_deepwell.sql
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ CREATE TYPE relation_object_type AS ENUM (

CREATE TABLE relation (
relation_id BIGSERIAL PRIMARY KEY,
relation_type TEXT NOT NULL, -- check enum value in runtime
relation_type TEXT NOT NULL, -- check enum value at runtime
dest_type relation_object_type NOT NULL,
dest_id BIGINT NOT NULL,
from_type relation_object_type NOT NULL,
Expand Down Expand Up @@ -696,3 +696,35 @@ CREATE TABLE filter (

UNIQUE (site_id, regex, deleted_at)
);

--
-- Audit Log
--

-- This very large table contains the platform's audit log.
--
-- Like relations, it is an algebraic data type, all rows
-- with the same event_type have the same data fields. These
-- columns are nullable to accomodate this.
--
-- Observe that there are no foreign keys here:
-- While we maintain that constraint in code, we are not
-- burdening Postgres with maintaining extremely large numbers
-- of these foreign keys.
--
CREATE TABLE audit_log (
event_id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL, -- check enum value at runtime
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
ip_address TEXT NOT NULL, -- TODO change to INET
user_id BIGINT,
site_id BIGINT,
page_id BIGINT,
extra_id_1 BIGINT,
extra_id_2 BIGINT,
extra_string_1 TEXT,
extra_string_2 TEXT,
extra_number INT,

CHECK (strpos(event_type, '.') != 0) -- all event types are '[object].[operation]'
);
10 changes: 10 additions & 0 deletions deepwell/src/database/seeder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::net::{IpAddr, Ipv6Addr};
use std::path::{Path, PathBuf};

/// The IP address to record for any seeded data.
/// This is just `localhost`.
pub const SEED_IP_ADDRESS: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));

pub async fn seed(state: &ServerState) -> Result<()> {
info!("Running seeder...");

Expand Down Expand Up @@ -94,13 +99,15 @@ pub async fn seed(state: &ServerState) -> Result<()> {
locales: user.locales,
bypass_filter: true,
bypass_email_verification: true,
ip_address: SEED_IP_ADDRESS,
},
)
.await?;

UserService::update(
&ctx,
Reference::Id(user_id),
SEED_IP_ADDRESS,
UpdateUserBody {
email_verified: Maybe::Set(true),
real_name: Maybe::Set(user.real_name),
Expand Down Expand Up @@ -161,6 +168,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
default_page: site.default_page,
layout: site.layout,
locale: site.locale,
ip_address: SEED_IP_ADDRESS,
},
)
.await?;
Expand Down Expand Up @@ -203,6 +211,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
..Default::default()
},
SYSTEM_USER_ID,
SEED_IP_ADDRESS,
)
.await?;

Expand Down Expand Up @@ -230,6 +239,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
revision_comments: str!(),
user_id: SYSTEM_USER_ID,
bypass_filter: true,
ip_address: SEED_IP_ADDRESS,
},
)
.await?;
Expand Down
48 changes: 18 additions & 30 deletions deepwell/src/endpoints/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ use crate::services::session::{
CreateSession, GetOtherSessions, GetOtherSessionsOutput, InvalidateOtherSessions,
RenewSession,
};
use crate::services::user::GetUser;

pub async fn auth_login(
ctx: &ServiceContext<'_>,
Expand Down Expand Up @@ -208,9 +207,16 @@ pub async fn auth_mfa_setup(
ctx: &ServiceContext<'_>,
params: Params<'static>,
) -> Result<MultiFactorSetupOutput> {
let GetUser { user: reference } = params.parse()?;
let user = UserService::get(ctx, reference).await?;
MfaService::setup(ctx, &user).await
let MultiFactorConfigure {
user_id,
session_token,
ip_address,
} = params.parse()?;

let user =
SessionService::get_user_with_id(ctx, &session_token, false, user_id).await?;

MfaService::setup(ctx, &user, ip_address).await
}

pub async fn auth_mfa_disable(
Expand All @@ -220,22 +226,13 @@ pub async fn auth_mfa_disable(
let MultiFactorConfigure {
user_id,
session_token,
ip_address,
} = params.parse()?;

let user = SessionService::get_user(ctx, &session_token, false).await?;
if user.user_id != user_id {
error!(
"Passed user ID ({}) does not match session token ({})",
user_id, user.user_id,
);

return Err(Error::SessionUserId {
active_user_id: user_id,
session_user_id: user.user_id,
});
}
let user =
SessionService::get_user_with_id(ctx, &session_token, false, user_id).await?;

MfaService::disable(ctx, user.user_id).await
MfaService::disable(ctx, user.user_id, ip_address).await
}

pub async fn auth_mfa_reset_recovery(
Expand All @@ -245,20 +242,11 @@ pub async fn auth_mfa_reset_recovery(
let MultiFactorConfigure {
user_id,
session_token,
ip_address,
} = params.parse()?;

let user = SessionService::get_user(ctx, &session_token, false).await?;
if user.user_id != user_id {
error!(
"Passed user ID ({}) does not match session token ({})",
user_id, user.user_id,
);

return Err(Error::SessionUserId {
active_user_id: user_id,
session_user_id: user.user_id,
});
}
let user =
SessionService::get_user_with_id(ctx, &session_token, false, user_id).await?;

MfaService::reset_recovery_codes(ctx, &user).await
MfaService::reset_recovery_codes(ctx, &user, ip_address).await
}
17 changes: 7 additions & 10 deletions deepwell/src/endpoints/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,23 +215,20 @@ pub async fn page_set_layout(
ctx: &ServiceContext<'_>,
params: Params<'static>,
) -> Result<()> {
let SetPageLayout {
site_id,
page_id,
layout,
} = params.parse()?;
let input: SetPageLayout = params.parse()?;

info!(
"Setting layout override for page {} in site ID {} to layout {}",
page_id,
site_id,
match layout {
"Setting layout override for page ID {} in site ID {} to layout {} (set by user ID {})",
input.page_id,
input.site_id,
match input.layout {
Some(layout) => layout.value(),
None => "none (default)",
},
input.user_id,
);

PageService::set_layout(ctx, site_id, page_id, layout).await
PageService::set_layout(ctx, input).await
}

async fn build_page_output(
Expand Down
3 changes: 2 additions & 1 deletion deepwell/src/endpoints/site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ pub async fn site_update(
site,
body,
user_id,
ip_address,
} = params.parse()?;

info!("Updating site {site:?}");
SiteService::update(ctx, site, body, user_id).await
SiteService::update(ctx, site, body, user_id, ip_address).await
}
3 changes: 2 additions & 1 deletion deepwell/src/endpoints/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,12 @@ pub async fn user_edit(
) -> Result<UserModel> {
let UpdateUser {
user: reference,
ip_address,
body,
} = params.parse()?;

info!("Updating user {reference:?}");
UserService::update(ctx, reference, body).await
UserService::update(ctx, reference, ip_address, body).await
}

pub async fn user_delete(
Expand Down
6 changes: 6 additions & 0 deletions deepwell/src/endpoints/user_bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::services::relation::{
};
use crate::services::user::{CreateUser, CreateUserOutput, GetUser, UpdateUserBody};
use crate::types::{Maybe, Reference};
use std::net::IpAddr;

// Structs

Expand All @@ -48,6 +49,8 @@ pub struct CreateBotUser {

#[serde(default)]
pub bypass_email_verification: bool,

pub ip_address: IpAddr,
}

/// Input structure for adding new bot owners.
Expand Down Expand Up @@ -78,6 +81,7 @@ pub async fn bot_user_create(
authorization_token,
bypass_filter,
bypass_email_verification,
ip_address,
} = params.parse()?;

info!("Creating new bot user with name '{name}'");
Expand All @@ -102,6 +106,7 @@ pub async fn bot_user_create(
password: String::new(), // TODO configure user-bot password
bypass_filter,
bypass_email_verification,
ip_address,
},
)
.await?;
Expand All @@ -111,6 +116,7 @@ pub async fn bot_user_create(
let bot_user = UserService::update(
ctx,
Reference::Id(bot_user_id),
ip_address,
UpdateUserBody {
biography: Maybe::Set(Some(purpose)),
..Default::default()
Expand Down
6 changes: 3 additions & 3 deletions deepwell/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,23 @@ macro_rules! str_writeln {
}};
}

/// Convenience function for creating a borrowed `Cow` from a string slice.
/// Convenience macro for creating a borrowed `Cow` from a string slice.
macro_rules! cow {
($s:expr) => {{
use std::borrow::Cow;
Cow::Borrowed($s.as_ref())
}};
}

/// Convenience function like `cow!`, but for `Option<Cow<str>>`.
/// Convenience macro like `cow!`, but for `Option<Cow<str>>`.
macro_rules! cow_opt {
($s:expr) => {{
use ref_map::*;
$s.ref_map(|s| cow!(s))
}};
}

/// Convenience function for making borrowed string `FluentValue`s.
/// Convenience macro for making borrowed string `FluentValue`s.
macro_rules! fluent_str {
($value:expr) => {
FluentValue::String(cow!(&$value))
Expand Down
32 changes: 32 additions & 0 deletions deepwell/src/models/audit_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10

use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "audit_log")]
pub struct Model {
#[sea_orm(primary_key)]
pub event_id: i64,
#[sea_orm(column_type = "Text")]
pub event_type: String,
#[serde(with = "time::serde::rfc3339")]
pub created_at: TimeDateTimeWithTimeZone,
#[sea_orm(column_type = "Text")]
pub ip_address: String,
pub user_id: Option<i64>,
pub site_id: Option<i64>,
pub page_id: Option<i64>,
pub extra_id_1: Option<i64>,
pub extra_id_2: Option<i64>,
#[sea_orm(column_type = "Text", nullable)]
pub extra_string_1: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
pub extra_string_2: Option<String>,
pub extra_number: Option<i32>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
1 change: 1 addition & 0 deletions deepwell/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pub mod prelude;

pub mod alias;
pub mod audit_log;
pub mod blob_blacklist;
pub mod blob_pending;
pub mod file;
Expand Down
41 changes: 0 additions & 41 deletions deepwell/src/models/user_bot_owner.rs

This file was deleted.

Loading
Loading