Skip to content

Commit 44107ea

Browse files
authored
feat: audit log (#1032)
Not great looking, but does the job. Planning to cleanup our admin dashboards in a follow-up today
1 parent a1d2f15 commit 44107ea

29 files changed

+1326
-243
lines changed

api/.sqlx/query-26883e51e0ec672545a9196d6c06f0adfa268dcde1dae8e8d22871de2007b920.json

Lines changed: 85 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/.sqlx/query-373d500106acb925a37dcd921bfc08245d2395f43b0c9044d1dca9920a52d099.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/.sqlx/query-e75e5494a0276741f29897324a2233d4b82c066781c172b28e771f2d9d70e8a8.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE audit_logs (
2+
actor_id uuid references users(id) NOT NULL,
3+
is_sudo bool NOT NULL,
4+
action text NOT NULL,
5+
meta jsonb NOT NULL,
6+
created_at timestamptz NOT NULL DEFAULT now()
7+
);

api/src/api/admin.rs

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use hyper::Body;
77
use hyper::Request;
88
use routerify::prelude::RequestExt;
99
use routerify::Router;
10+
use routerify_query::RequestQueryExt;
1011
use tracing::field;
1112
use tracing::instrument;
1213
use tracing::Instrument;
@@ -46,6 +47,7 @@ pub fn admin_router() -> Router<Body, ApiError> {
4647
)
4748
.get("/tickets", util::auth(util::json(list_tickets)))
4849
.patch("/tickets/:id", util::auth(util::json(patch_ticket)))
50+
.get("/audit_logs", util::auth(util::json(list_audit_logs)))
4951
.build()
5052
.unwrap()
5153
}
@@ -112,9 +114,6 @@ pub async fn list_users(req: Request<Body>) -> ApiResult<ApiList<ApiFullUser>> {
112114
fields(user_id)
113115
)]
114116
pub async fn update_user(mut req: Request<Body>) -> ApiResult<ApiFullUser> {
115-
let iam = req.iam();
116-
iam.check_admin_access()?;
117-
118117
let user_id = req.param_uuid("user_id")?;
119118
Span::current().record("user_id", field::display(&user_id));
120119
let ApiAdminUpdateUserRequest {
@@ -124,16 +123,23 @@ pub async fn update_user(mut req: Request<Body>) -> ApiResult<ApiFullUser> {
124123
} = decode_json(&mut req).await?;
125124
let db = req.data::<Database>().unwrap();
126125

126+
let iam = req.iam();
127+
let staff = iam.check_admin_access()?;
128+
127129
let mut updated_user = None;
128130

129131
if let Some(is_staff) = is_staff {
130-
updated_user = Some(db.user_set_staff(user_id, is_staff).await?);
132+
updated_user = Some(db.user_set_staff(&staff.id, user_id, is_staff).await?);
131133
}
132134
if let Some(is_blocked) = is_blocked {
133-
updated_user = Some(db.user_set_blocked(user_id, is_blocked).await?);
135+
updated_user =
136+
Some(db.user_set_blocked(&staff.id, user_id, is_blocked).await?);
134137
}
135138
if let Some(scope_limit) = scope_limit {
136-
updated_user = Some(db.user_set_scope_limit(user_id, scope_limit).await?);
139+
updated_user = Some(
140+
db.user_set_scope_limit(&staff.id, user_id, scope_limit)
141+
.await?,
142+
);
137143
}
138144

139145
if let Some(updated_user) = updated_user {
@@ -170,9 +176,6 @@ pub async fn list_scopes(
170176
fields(scope)
171177
)]
172178
pub async fn patch_scopes(mut req: Request<Body>) -> ApiResult<ApiFullScope> {
173-
let iam = req.iam();
174-
iam.check_admin_access()?;
175-
176179
let scope = req.param_scope()?;
177180
Span::current().record("scope", field::display(&scope));
178181

@@ -182,6 +185,9 @@ pub async fn patch_scopes(mut req: Request<Body>) -> ApiResult<ApiFullScope> {
182185
publish_attempts_per_week_limit,
183186
} = decode_json(&mut req).await?;
184187

188+
let iam = req.iam();
189+
let staff = iam.check_admin_access()?;
190+
185191
let db = req.data::<Database>().unwrap();
186192

187193
if package_limit.is_none()
@@ -195,6 +201,7 @@ pub async fn patch_scopes(mut req: Request<Body>) -> ApiResult<ApiFullScope> {
195201

196202
let scope = db
197203
.update_scope_limits(
204+
&staff.id,
198205
&scope,
199206
package_limit,
200207
new_package_per_week_limit,
@@ -212,13 +219,13 @@ pub async fn patch_scopes(mut req: Request<Body>) -> ApiResult<ApiFullScope> {
212219
fields(scope, user_id)
213220
)]
214221
pub async fn assign_scope(mut req: Request<Body>) -> ApiResult<ApiScope> {
215-
let iam = req.iam();
216-
iam.check_admin_access()?;
217-
218222
let ApiAssignScopeRequest { scope, user_id } = decode_json(&mut req).await?;
219223
Span::current().record("scope", field::display(&scope));
220224
Span::current().record("user_id", field::display(&user_id));
221225

226+
let iam = req.iam();
227+
let staff = iam.check_admin_access()?;
228+
222229
let db = req.data::<Database>().unwrap();
223230

224231
let scope_without_hyphens = scope.replace('-', "");
@@ -228,7 +235,7 @@ pub async fn assign_scope(mut req: Request<Body>) -> ApiResult<ApiScope> {
228235
}
229236

230237
let scope = db
231-
.create_scope(&scope, user_id)
238+
.create_scope(&staff.id, true, &scope, user_id)
232239
.await
233240
.map_err(|e| map_unique_violation(e, ApiError::ScopeAlreadyExists))?;
234241

@@ -266,7 +273,7 @@ pub async fn list_publishing_tasks(
266273
)]
267274
pub async fn requeue_publishing_tasks(req: Request<Body>) -> ApiResult<()> {
268275
let iam = req.iam();
269-
iam.check_admin_access()?;
276+
let staff = iam.check_admin_access()?;
270277

271278
let publishing_task_id = req.param_uuid("publishing_task")?;
272279
Span::current()
@@ -280,6 +287,7 @@ pub async fn requeue_publishing_tasks(req: Request<Body>) -> ApiResult<()> {
280287

281288
if task.status == PublishingTaskStatus::Processing {
282289
db.update_publishing_task_status(
290+
Some(&staff.id),
283291
publishing_task_id,
284292
PublishingTaskStatus::Processing,
285293
PublishingTaskStatus::Pending,
@@ -292,7 +300,7 @@ pub async fn requeue_publishing_tasks(req: Request<Body>) -> ApiResult<()> {
292300
let orama_client = req.data::<Option<OramaClient>>().unwrap().clone();
293301

294302
if let Some(queue) = publish_queue {
295-
let body = serde_json::to_vec(&publishing_task_id).unwrap();
303+
let body = serde_json::to_vec(&publishing_task_id)?;
296304
queue.task_buffer(None, Some(body.into())).await?;
297305
} else {
298306
let buckets = req.data::<Buckets>().unwrap().clone();
@@ -326,25 +334,25 @@ pub async fn list_tickets(req: Request<Body>) -> ApiResult<ApiList<ApiTicket>> {
326334

327335
let (total, tickets) = db.list_tickets(start, limit, maybe_search).await?;
328336
Ok(ApiList {
329-
items: tickets.into_iter().map(|scope| scope.into()).collect(),
337+
items: tickets.into_iter().map(|ticket| ticket.into()).collect(),
330338
total,
331339
})
332340
}
333341

334342
#[instrument(name = "PATCH /api/admin/tickets/:id", skip(req), err)]
335343
pub async fn patch_ticket(mut req: Request<Body>) -> ApiResult<ApiTicket> {
336-
let iam = req.iam();
337-
iam.check_admin_access()?;
338-
339344
let id = req.param_uuid("id")?;
340345
Span::current().record("id", field::display(id));
341346

342347
let ApiAdminUpdateTicketRequest { closed } = decode_json(&mut req).await?;
343348

349+
let iam = req.iam();
350+
let staff = iam.check_admin_access()?;
351+
344352
let db = req.data::<Database>().unwrap();
345353

346354
let ticket = if let Some(closed) = closed {
347-
db.update_ticket_closed(id, closed).await?
355+
db.update_ticket_closed(&staff.id, id, closed).await?
348356
} else {
349357
return Err(ApiError::MalformedRequest {
350358
msg: "missing 'closed' parameter".into(),
@@ -354,6 +362,30 @@ pub async fn patch_ticket(mut req: Request<Body>) -> ApiResult<ApiTicket> {
354362
Ok(ticket.into())
355363
}
356364

365+
#[instrument(name = "GET /api/admin/audit_logs", skip(req), err)]
366+
pub async fn list_audit_logs(
367+
req: Request<Body>,
368+
) -> ApiResult<ApiList<ApiAuditLog>> {
369+
let iam = req.iam();
370+
iam.check_admin_access()?;
371+
372+
let db = req.data::<Database>().unwrap();
373+
let (start, limit) = pagination(&req);
374+
let maybe_search = search(&req);
375+
let sudo_only = req.query("sudoOnly").is_some();
376+
377+
let (total, audit_logs) = db
378+
.list_audit_logs(start, limit, maybe_search, sudo_only)
379+
.await?;
380+
Ok(ApiList {
381+
items: audit_logs
382+
.into_iter()
383+
.map(|audit_log| audit_log.into())
384+
.collect(),
385+
total,
386+
})
387+
}
388+
357389
#[cfg(test)]
358390
mod tests {
359391
use crate::api::ApiFullScope;

0 commit comments

Comments
 (0)