Skip to content

Commit 2417df0

Browse files
authored
Merge pull request #2535 from scpwiki/WJ-192-audit-log
[WJ-192] Add audit log
2 parents e800fb7 + df2167b commit 2417df0

File tree

29 files changed

+1032
-117
lines changed

29 files changed

+1032
-117
lines changed

deepwell/migrations/20220906103252_deepwell.sql

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ CREATE TYPE relation_object_type AS ENUM (
159159

160160
CREATE TABLE relation (
161161
relation_id BIGSERIAL PRIMARY KEY,
162-
relation_type TEXT NOT NULL, -- check enum value in runtime
162+
relation_type TEXT NOT NULL, -- check enum value at runtime
163163
dest_type relation_object_type NOT NULL,
164164
dest_id BIGINT NOT NULL,
165165
from_type relation_object_type NOT NULL,
@@ -696,3 +696,35 @@ CREATE TABLE filter (
696696

697697
UNIQUE (site_id, regex, deleted_at)
698698
);
699+
700+
--
701+
-- Audit Log
702+
--
703+
704+
-- This very large table contains the platform's audit log.
705+
--
706+
-- Like relations, it is an algebraic data type, all rows
707+
-- with the same event_type have the same data fields. These
708+
-- columns are nullable to accomodate this.
709+
--
710+
-- Observe that there are no foreign keys here:
711+
-- While we maintain that constraint in code, we are not
712+
-- burdening Postgres with maintaining extremely large numbers
713+
-- of these foreign keys.
714+
--
715+
CREATE TABLE audit_log (
716+
event_id BIGSERIAL PRIMARY KEY,
717+
event_type TEXT NOT NULL, -- check enum value at runtime
718+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
719+
ip_address TEXT NOT NULL, -- TODO change to INET
720+
user_id BIGINT,
721+
site_id BIGINT,
722+
page_id BIGINT,
723+
extra_id_1 BIGINT,
724+
extra_id_2 BIGINT,
725+
extra_string_1 TEXT,
726+
extra_string_2 TEXT,
727+
extra_number INT,
728+
729+
CHECK (strpos(event_type, '.') != 0) -- all event types are '[object].[operation]'
730+
);

deepwell/src/database/seeder/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ use std::borrow::Cow;
4343
use std::collections::HashMap;
4444
use std::fs;
4545
use std::io::Read;
46+
use std::net::{IpAddr, Ipv6Addr};
4647
use std::path::{Path, PathBuf};
4748

49+
/// The IP address to record for any seeded data.
50+
/// This is just `localhost`.
51+
pub const SEED_IP_ADDRESS: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
52+
4853
pub async fn seed(state: &ServerState) -> Result<()> {
4954
info!("Running seeder...");
5055

@@ -94,13 +99,15 @@ pub async fn seed(state: &ServerState) -> Result<()> {
9499
locales: user.locales,
95100
bypass_filter: true,
96101
bypass_email_verification: true,
102+
ip_address: SEED_IP_ADDRESS,
97103
},
98104
)
99105
.await?;
100106

101107
UserService::update(
102108
&ctx,
103109
Reference::Id(user_id),
110+
SEED_IP_ADDRESS,
104111
UpdateUserBody {
105112
email_verified: Maybe::Set(true),
106113
real_name: Maybe::Set(user.real_name),
@@ -161,6 +168,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
161168
default_page: site.default_page,
162169
layout: site.layout,
163170
locale: site.locale,
171+
ip_address: SEED_IP_ADDRESS,
164172
},
165173
)
166174
.await?;
@@ -203,6 +211,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
203211
..Default::default()
204212
},
205213
SYSTEM_USER_ID,
214+
SEED_IP_ADDRESS,
206215
)
207216
.await?;
208217

@@ -230,6 +239,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
230239
revision_comments: str!(),
231240
user_id: SYSTEM_USER_ID,
232241
bypass_filter: true,
242+
ip_address: SEED_IP_ADDRESS,
233243
},
234244
)
235245
.await?;

deepwell/src/endpoints/auth.rs

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ use crate::services::session::{
3232
CreateSession, GetOtherSessions, GetOtherSessionsOutput, InvalidateOtherSessions,
3333
RenewSession,
3434
};
35-
use crate::services::user::GetUser;
3635

3736
pub async fn auth_login(
3837
ctx: &ServiceContext<'_>,
@@ -208,9 +207,16 @@ pub async fn auth_mfa_setup(
208207
ctx: &ServiceContext<'_>,
209208
params: Params<'static>,
210209
) -> Result<MultiFactorSetupOutput> {
211-
let GetUser { user: reference } = params.parse()?;
212-
let user = UserService::get(ctx, reference).await?;
213-
MfaService::setup(ctx, &user).await
210+
let MultiFactorConfigure {
211+
user_id,
212+
session_token,
213+
ip_address,
214+
} = params.parse()?;
215+
216+
let user =
217+
SessionService::get_user_with_id(ctx, &session_token, false, user_id).await?;
218+
219+
MfaService::setup(ctx, &user, ip_address).await
214220
}
215221

216222
pub async fn auth_mfa_disable(
@@ -220,22 +226,13 @@ pub async fn auth_mfa_disable(
220226
let MultiFactorConfigure {
221227
user_id,
222228
session_token,
229+
ip_address,
223230
} = params.parse()?;
224231

225-
let user = SessionService::get_user(ctx, &session_token, false).await?;
226-
if user.user_id != user_id {
227-
error!(
228-
"Passed user ID ({}) does not match session token ({})",
229-
user_id, user.user_id,
230-
);
231-
232-
return Err(Error::SessionUserId {
233-
active_user_id: user_id,
234-
session_user_id: user.user_id,
235-
});
236-
}
232+
let user =
233+
SessionService::get_user_with_id(ctx, &session_token, false, user_id).await?;
237234

238-
MfaService::disable(ctx, user.user_id).await
235+
MfaService::disable(ctx, user.user_id, ip_address).await
239236
}
240237

241238
pub async fn auth_mfa_reset_recovery(
@@ -245,20 +242,11 @@ pub async fn auth_mfa_reset_recovery(
245242
let MultiFactorConfigure {
246243
user_id,
247244
session_token,
245+
ip_address,
248246
} = params.parse()?;
249247

250-
let user = SessionService::get_user(ctx, &session_token, false).await?;
251-
if user.user_id != user_id {
252-
error!(
253-
"Passed user ID ({}) does not match session token ({})",
254-
user_id, user.user_id,
255-
);
256-
257-
return Err(Error::SessionUserId {
258-
active_user_id: user_id,
259-
session_user_id: user.user_id,
260-
});
261-
}
248+
let user =
249+
SessionService::get_user_with_id(ctx, &session_token, false, user_id).await?;
262250

263-
MfaService::reset_recovery_codes(ctx, &user).await
251+
MfaService::reset_recovery_codes(ctx, &user, ip_address).await
264252
}

deepwell/src/endpoints/page.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,23 +215,20 @@ pub async fn page_set_layout(
215215
ctx: &ServiceContext<'_>,
216216
params: Params<'static>,
217217
) -> Result<()> {
218-
let SetPageLayout {
219-
site_id,
220-
page_id,
221-
layout,
222-
} = params.parse()?;
218+
let input: SetPageLayout = params.parse()?;
223219

224220
info!(
225-
"Setting layout override for page {} in site ID {} to layout {}",
226-
page_id,
227-
site_id,
228-
match layout {
221+
"Setting layout override for page ID {} in site ID {} to layout {} (set by user ID {})",
222+
input.page_id,
223+
input.site_id,
224+
match input.layout {
229225
Some(layout) => layout.value(),
230226
None => "none (default)",
231227
},
228+
input.user_id,
232229
);
233230

234-
PageService::set_layout(ctx, site_id, page_id, layout).await
231+
PageService::set_layout(ctx, input).await
235232
}
236233

237234
async fn build_page_output(

deepwell/src/endpoints/site.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ pub async fn site_update(
6565
site,
6666
body,
6767
user_id,
68+
ip_address,
6869
} = params.parse()?;
6970

7071
info!("Updating site {site:?}");
71-
SiteService::update(ctx, site, body, user_id).await
72+
SiteService::update(ctx, site, body, user_id, ip_address).await
7273
}

deepwell/src/endpoints/user.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,12 @@ pub async fn user_edit(
6666
) -> Result<UserModel> {
6767
let UpdateUser {
6868
user: reference,
69+
ip_address,
6970
body,
7071
} = params.parse()?;
7172

7273
info!("Updating user {reference:?}");
73-
UserService::update(ctx, reference, body).await
74+
UserService::update(ctx, reference, ip_address, body).await
7475
}
7576

7677
pub async fn user_delete(

deepwell/src/endpoints/user_bot.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::services::relation::{
2626
};
2727
use crate::services::user::{CreateUser, CreateUserOutput, GetUser, UpdateUserBody};
2828
use crate::types::{Maybe, Reference};
29+
use std::net::IpAddr;
2930

3031
// Structs
3132

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

4950
#[serde(default)]
5051
pub bypass_email_verification: bool,
52+
53+
pub ip_address: IpAddr,
5154
}
5255

5356
/// Input structure for adding new bot owners.
@@ -78,6 +81,7 @@ pub async fn bot_user_create(
7881
authorization_token,
7982
bypass_filter,
8083
bypass_email_verification,
84+
ip_address,
8185
} = params.parse()?;
8286

8387
info!("Creating new bot user with name '{name}'");
@@ -102,6 +106,7 @@ pub async fn bot_user_create(
102106
password: String::new(), // TODO configure user-bot password
103107
bypass_filter,
104108
bypass_email_verification,
109+
ip_address,
105110
},
106111
)
107112
.await?;
@@ -111,6 +116,7 @@ pub async fn bot_user_create(
111116
let bot_user = UserService::update(
112117
ctx,
113118
Reference::Id(bot_user_id),
119+
ip_address,
114120
UpdateUserBody {
115121
biography: Maybe::Set(Some(purpose)),
116122
..Default::default()

deepwell/src/macros.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,23 @@ macro_rules! str_writeln {
4646
}};
4747
}
4848

49-
/// Convenience function for creating a borrowed `Cow` from a string slice.
49+
/// Convenience macro for creating a borrowed `Cow` from a string slice.
5050
macro_rules! cow {
5151
($s:expr) => {{
5252
use std::borrow::Cow;
5353
Cow::Borrowed($s.as_ref())
5454
}};
5555
}
5656

57-
/// Convenience function like `cow!`, but for `Option<Cow<str>>`.
57+
/// Convenience macro like `cow!`, but for `Option<Cow<str>>`.
5858
macro_rules! cow_opt {
5959
($s:expr) => {{
6060
use ref_map::*;
6161
$s.ref_map(|s| cow!(s))
6262
}};
6363
}
6464

65-
/// Convenience function for making borrowed string `FluentValue`s.
65+
/// Convenience macro for making borrowed string `FluentValue`s.
6666
macro_rules! fluent_str {
6767
($value:expr) => {
6868
FluentValue::String(cow!(&$value))

deepwell/src/models/audit_log.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.10
2+
3+
use sea_orm::entity::prelude::*;
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
7+
#[sea_orm(table_name = "audit_log")]
8+
pub struct Model {
9+
#[sea_orm(primary_key)]
10+
pub event_id: i64,
11+
#[sea_orm(column_type = "Text")]
12+
pub event_type: String,
13+
#[serde(with = "time::serde::rfc3339")]
14+
pub created_at: TimeDateTimeWithTimeZone,
15+
#[sea_orm(column_type = "Text")]
16+
pub ip_address: String,
17+
pub user_id: Option<i64>,
18+
pub site_id: Option<i64>,
19+
pub page_id: Option<i64>,
20+
pub extra_id_1: Option<i64>,
21+
pub extra_id_2: Option<i64>,
22+
#[sea_orm(column_type = "Text", nullable)]
23+
pub extra_string_1: Option<String>,
24+
#[sea_orm(column_type = "Text", nullable)]
25+
pub extra_string_2: Option<String>,
26+
pub extra_number: Option<i32>,
27+
}
28+
29+
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
30+
pub enum Relation {}
31+
32+
impl ActiveModelBehavior for ActiveModel {}

deepwell/src/models/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
pub mod prelude;
44

55
pub mod alias;
6+
pub mod audit_log;
67
pub mod blob_blacklist;
78
pub mod blob_pending;
89
pub mod file;

0 commit comments

Comments
 (0)