Skip to content

Commit b413b4c

Browse files
committed
Add Crate Owner Auditing
Including the following audit actions: - Invite User - Remove User Closes #1548
1 parent face1f3 commit b413b4c

File tree

12 files changed

+297
-21
lines changed

12 files changed

+297
-21
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE crate_owner_actions;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE crate_owner_actions (
2+
id SERIAL PRIMARY KEY,
3+
crate_id INTEGER NOT NULL REFERENCES crates(id) ON DELETE CASCADE,
4+
user_id INTEGER NOT NULL REFERENCES users(id),
5+
api_token_id INTEGER REFERENCES api_tokens(id),
6+
action INTEGER NOT NULL,
7+
time TIMESTAMP NOT NULL DEFAULT now()
8+
);

src/controllers/krate/metadata.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
77
use crate::controllers::frontend_prelude::*;
88
use crate::models::{
9-
Category, Crate, CrateCategory, CrateKeyword, CrateVersions, Keyword, RecentCrateDownloads,
10-
User, Version, VersionOwnerAction,
9+
Category, Crate, CrateCategory, CrateKeyword, CrateOwnerAction, CrateVersions, Keyword,
10+
RecentCrateDownloads, User, Version, VersionOwnerAction,
1111
};
1212
use crate::schema::*;
1313
use crate::views::{
@@ -105,6 +105,7 @@ pub fn show(req: &mut dyn Request) -> AppResult<Response> {
105105
let conn = req.db_conn()?;
106106
let krate = Crate::by_name(name).first::<Crate>(&*conn)?;
107107

108+
let krate_owner_actions = CrateOwnerAction::by_crate(&*conn, &krate)?;
108109
let mut versions_and_publishers = krate
109110
.all_versions()
110111
.left_outer_join(users::table)
@@ -161,6 +162,7 @@ pub fn show(req: &mut dyn Request) -> AppResult<Response> {
161162
Some(badges),
162163
false,
163164
recent_downloads,
165+
krate_owner_actions,
164166
),
165167
versions: versions_publishers_and_audit_actions
166168
.into_iter()

src/controllers/krate/owners.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use serde_json;
44

55
use crate::controllers::prelude::*;
6-
use crate::models::{Crate, Owner, Rights, Team, User};
6+
use crate::models::{insert_crate_owner_action, Crate, CrateAction, Owner, Rights, Team, User};
77
use crate::views::EncodableOwner;
88

99
/// Handles the `GET /crates/:crate_id/owners` route.
@@ -95,6 +95,7 @@ fn modify_owners(req: &mut dyn Request, add: bool) -> AppResult<Response> {
9595
let user = req.user()?;
9696
let crate_name = &req.params()["crate_id"];
9797
let conn = req.db_conn()?;
98+
let authentication_source = req.authentication_source()?;
9899

99100
conn.transaction(|| {
100101
let krate = Crate::by_name(crate_name).first::<Crate>(&*conn)?;
@@ -124,6 +125,15 @@ fn modify_owners(req: &mut dyn Request, add: bool) -> AppResult<Response> {
124125
let msg = krate.owner_add(app, &conn, user, login)?;
125126
msgs.push(msg);
126127
}
128+
129+
insert_crate_owner_action(
130+
&conn,
131+
krate.id,
132+
user.id,
133+
authentication_source.api_token_id(),
134+
CrateAction::InviteUser,
135+
)?;
136+
127137
msgs.join(",")
128138
} else {
129139
for login in &logins {
@@ -136,6 +146,15 @@ fn modify_owners(req: &mut dyn Request, add: bool) -> AppResult<Response> {
136146
at least one individual owner is required.",
137147
));
138148
}
149+
150+
insert_crate_owner_action(
151+
&conn,
152+
krate.id,
153+
user.id,
154+
authentication_source.api_token_id(),
155+
CrateAction::RemoveUser,
156+
)?;
157+
139158
"owners successfully removed".to_owned()
140159
};
141160

src/models.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub use self::email::{Email, NewEmail};
88
pub use self::follow::Follow;
99
pub use self::keyword::{CrateKeyword, Keyword};
1010
pub use self::krate::{Crate, CrateVersions, NewCrate, RecentCrateDownloads};
11+
pub use self::krate_owner_actions::{insert_crate_owner_action, CrateAction, CrateOwnerAction};
1112
pub use self::owner::{CrateOwner, Owner, OwnerKind};
1213
pub use self::rights::Rights;
1314
pub use self::team::{NewTeam, Team};
@@ -27,6 +28,7 @@ mod email;
2728
mod follow;
2829
mod keyword;
2930
pub mod krate;
31+
mod krate_owner_actions;
3032
mod owner;
3133
mod rights;
3234
mod team;

src/models/krate.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use crate::email;
1111
use crate::util::{cargo_err, AppResult};
1212

1313
use crate::models::{
14-
Badge, Category, CrateOwner, CrateOwnerInvitation, Keyword, NewCrateOwnerInvitation, Owner,
15-
OwnerKind, ReverseDependency, User, Version,
14+
Badge, Category, CrateOwner, CrateOwnerAction, CrateOwnerInvitation, Keyword,
15+
NewCrateOwnerInvitation, Owner, OwnerKind, ReverseDependency, User, Version,
1616
};
17-
use crate::views::{EncodableCrate, EncodableCrateLinks};
17+
use crate::views::{EncodableAuditAction, EncodableCrate, EncodableCrateLinks};
1818

1919
use crate::models::helpers::with_count::*;
2020
use crate::publish_rate_limit::PublishRateLimit;
@@ -294,6 +294,7 @@ impl Crate {
294294
badges,
295295
exact_match,
296296
recent_downloads,
297+
vec![],
297298
)
298299
}
299300

@@ -307,6 +308,7 @@ impl Crate {
307308
badges: Option<Vec<Badge>>,
308309
exact_match: bool,
309310
recent_downloads: Option<i64>,
311+
audit_actions: Vec<(CrateOwnerAction, User)>,
310312
) -> EncodableCrate {
311313
let Crate {
312314
name,
@@ -327,6 +329,14 @@ impl Crate {
327329
let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect());
328330
let badges = badges.map(|bs| bs.into_iter().map(Badge::encodable).collect());
329331
let documentation = Crate::remove_blocked_documentation_urls(documentation);
332+
let audit_actions = audit_actions
333+
.into_iter()
334+
.map(|(audit_action, user)| EncodableAuditAction {
335+
action: audit_action.action.into(),
336+
user: User::encodable_public(user),
337+
time: audit_action.time,
338+
})
339+
.collect();
330340

331341
EncodableCrate {
332342
id: name.clone(),
@@ -353,6 +363,7 @@ impl Crate {
353363
owner_user: Some(format!("/api/v1/crates/{}/owner_user", name)),
354364
reverse_dependencies: format!("/api/v1/crates/{}/reverse_dependencies", name),
355365
},
366+
audit_actions,
356367
}
357368
}
358369

src/models/krate_owner_actions.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use chrono::NaiveDateTime;
2+
use diesel::prelude::*;
3+
use diesel::{
4+
deserialize::{self, FromSql},
5+
pg::Pg,
6+
serialize::{self, Output, ToSql},
7+
sql_types::Integer,
8+
};
9+
use std::io::Write;
10+
11+
use crate::models::{ApiToken, Crate, User};
12+
use crate::schema::*;
13+
14+
#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)]
15+
#[repr(i32)]
16+
#[sql_type = "Integer"]
17+
pub enum CrateAction {
18+
InviteUser = 0,
19+
RemoveUser = 1,
20+
}
21+
22+
impl Into<&'static str> for CrateAction {
23+
fn into(self) -> &'static str {
24+
match self {
25+
CrateAction::InviteUser => "invite_user",
26+
CrateAction::RemoveUser => "remove_user",
27+
}
28+
}
29+
}
30+
31+
impl Into<String> for CrateAction {
32+
fn into(self) -> String {
33+
let string: &'static str = self.into();
34+
35+
string.into()
36+
}
37+
}
38+
39+
impl FromSql<Integer, Pg> for CrateAction {
40+
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
41+
match <i32 as FromSql<Integer, Pg>>::from_sql(bytes)? {
42+
0 => Ok(CrateAction::InviteUser),
43+
1 => Ok(CrateAction::RemoveUser),
44+
n => Err(format!("unknown crate action: {}", n).into()),
45+
}
46+
}
47+
}
48+
49+
impl ToSql<Integer, Pg> for CrateAction {
50+
fn to_sql<W: Write>(&self, out: &mut Output<'_, W, Pg>) -> serialize::Result {
51+
ToSql::<Integer, Pg>::to_sql(&(*self as i32), out)
52+
}
53+
}
54+
55+
#[derive(Debug, Clone, Copy, Queryable, Identifiable, Associations)]
56+
#[belongs_to(Crate)]
57+
#[belongs_to(User)]
58+
#[belongs_to(ApiToken)]
59+
pub struct CrateOwnerAction {
60+
pub id: i32,
61+
pub crate_id: i32,
62+
pub user_id: i32,
63+
pub api_token_id: Option<i32>,
64+
pub action: CrateAction,
65+
pub time: NaiveDateTime,
66+
}
67+
68+
impl CrateOwnerAction {
69+
pub fn all(conn: &PgConnection) -> QueryResult<Vec<Self>> {
70+
crate_owner_actions::table.load(conn)
71+
}
72+
73+
pub fn by_crate(conn: &PgConnection, krate: &Crate) -> QueryResult<Vec<(Self, User)>> {
74+
use crate_owner_actions::dsl::crate_id;
75+
76+
crate_owner_actions::table
77+
.filter(crate_id.eq(krate.id))
78+
.inner_join(users::table)
79+
.order(crate_owner_actions::dsl::id)
80+
.load(conn)
81+
}
82+
}
83+
84+
pub fn insert_crate_owner_action(
85+
conn: &PgConnection,
86+
crate_id_: i32,
87+
user_id_: i32,
88+
api_token_id_: Option<i32>,
89+
action_: CrateAction,
90+
) -> QueryResult<CrateOwnerAction> {
91+
use crate_owner_actions::dsl::{action, api_token_id, crate_id, user_id};
92+
93+
diesel::insert_into(crate_owner_actions::table)
94+
.values((
95+
crate_id.eq(crate_id_),
96+
user_id.eq(user_id_),
97+
api_token_id.eq(api_token_id_),
98+
action.eq(action_),
99+
))
100+
.get_result(conn)
101+
}

src/schema.patch

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ index df884e4..18e08cd 100644
1010
use diesel_full_text_search::{TsVector as Tsvector};
1111

1212
/// Representation of the `api_tokens` table.
13-
@@ -125,14 +125,8 @@ table! {
13+
@@ -169,16 +171,10 @@
14+
///
1415
/// Its SQL type is `Timestamp`.
1516
///
1617
/// (Automatically generated by Diesel.)
@@ -25,8 +26,8 @@ index df884e4..18e08cd 100644
2526
}
2627

2728
table! {
28-
@@ -608,11 +610,29 @@ table! {
29-
/// (Automatically generated by Diesel.)
29+
use diesel::sql_types::*;
30+
@@ -711,10 +707,28 @@
3031
rendered_at -> Timestamp,
3132
}
3233
}
@@ -55,10 +56,10 @@ index df884e4..18e08cd 100644
5556

5657
/// Representation of the `reserved_crate_names` table.
5758
///
58-
@@ -881,23 +901,25 @@ table! {
59-
60-
joinable!(api_tokens -> users (user_id));
61-
joinable!(badges -> crates (crate_id));
59+
@@ -1045,11 +1059,12 @@
60+
joinable!(crate_owner_actions -> api_tokens (api_token_id));
61+
joinable!(crate_owner_actions -> crates (crate_id));
62+
joinable!(crate_owner_actions -> users (user_id));
6263
joinable!(crate_owner_invitations -> crates (crate_id));
6364
joinable!(crate_owners -> crates (crate_id));
6465
-joinable!(crate_owners -> users (created_by));
@@ -69,8 +70,7 @@ index df884e4..18e08cd 100644
6970
joinable!(crates_keywords -> crates (crate_id));
7071
joinable!(crates_keywords -> keywords (keyword_id));
7172
joinable!(dependencies -> crates (crate_id));
72-
joinable!(dependencies -> versions (version_id));
73-
joinable!(emails -> users (user_id));
73+
@@ -1058,10 +1073,11 @@
7474
joinable!(follows -> crates (crate_id));
7575
joinable!(follows -> users (user_id));
7676
joinable!(publish_limit_buckets -> users (user_id));
@@ -80,11 +80,9 @@ index df884e4..18e08cd 100644
8080
joinable!(version_authors -> users (user_id));
8181
joinable!(version_authors -> versions (version_id));
8282
joinable!(version_downloads -> versions (version_id));
83-
joinable!(version_owner_actions -> api_tokens (owner_token_id));
84-
85-
@@ -913,13 +935,14 @@ allow_tables_to_appear_in_same_query!(
86-
emails,
87-
follows,
83+
joinable!(version_owner_actions -> api_tokens (api_token_id));
84+
joinable!(version_owner_actions -> users (user_id));
85+
@@ -1087,10 +1103,11 @@
8886
keywords,
8987
metadata,
9088
publish_limit_buckets,
@@ -96,4 +94,3 @@ index df884e4..18e08cd 100644
9694
users,
9795
version_authors,
9896
version_downloads,
99-
versions,

src/schema.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,53 @@ table! {
176176
}
177177
}
178178

179+
table! {
180+
use diesel::sql_types::*;
181+
use diesel_full_text_search::{TsVector as Tsvector};
182+
183+
/// Representation of the `crate_owner_actions` table.
184+
///
185+
/// (Automatically generated by Diesel.)
186+
crate_owner_actions (id) {
187+
/// The `id` column of the `crate_owner_actions` table.
188+
///
189+
/// Its SQL type is `Int4`.
190+
///
191+
/// (Automatically generated by Diesel.)
192+
id -> Int4,
193+
/// The `crate_id` column of the `crate_owner_actions` table.
194+
///
195+
/// Its SQL type is `Int4`.
196+
///
197+
/// (Automatically generated by Diesel.)
198+
crate_id -> Int4,
199+
/// The `user_id` column of the `crate_owner_actions` table.
200+
///
201+
/// Its SQL type is `Int4`.
202+
///
203+
/// (Automatically generated by Diesel.)
204+
user_id -> Int4,
205+
/// The `api_token_id` column of the `crate_owner_actions` table.
206+
///
207+
/// Its SQL type is `Nullable<Int4>`.
208+
///
209+
/// (Automatically generated by Diesel.)
210+
api_token_id -> Nullable<Int4>,
211+
/// The `action` column of the `crate_owner_actions` table.
212+
///
213+
/// Its SQL type is `Int4`.
214+
///
215+
/// (Automatically generated by Diesel.)
216+
action -> Int4,
217+
/// The `time` column of the `crate_owner_actions` table.
218+
///
219+
/// Its SQL type is `Timestamp`.
220+
///
221+
/// (Automatically generated by Diesel.)
222+
time -> Timestamp,
223+
}
224+
}
225+
179226
table! {
180227
use diesel::sql_types::*;
181228
use diesel_full_text_search::{TsVector as Tsvector};
@@ -1009,6 +1056,9 @@ table! {
10091056

10101057
joinable!(api_tokens -> users (user_id));
10111058
joinable!(badges -> crates (crate_id));
1059+
joinable!(crate_owner_actions -> api_tokens (api_token_id));
1060+
joinable!(crate_owner_actions -> crates (crate_id));
1061+
joinable!(crate_owner_actions -> users (user_id));
10121062
joinable!(crate_owner_invitations -> crates (crate_id));
10131063
joinable!(crate_owners -> crates (crate_id));
10141064
joinable!(crate_owners -> teams (owner_id));
@@ -1041,6 +1091,7 @@ allow_tables_to_appear_in_same_query!(
10411091
background_jobs,
10421092
badges,
10431093
categories,
1094+
crate_owner_actions,
10441095
crate_owner_invitations,
10451096
crate_owners,
10461097
crates,

0 commit comments

Comments
 (0)