Skip to content

Commit 653a122

Browse files
committed
Adding audit trail actions into the publish, yank and unyank transactions.
1 parent ccfda71 commit 653a122

File tree

11 files changed

+474
-37
lines changed

11 files changed

+474
-37
lines changed

src/controllers/krate/publish.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ use swirl::Job;
77
use crate::controllers::prelude::*;
88
use crate::git;
99
use crate::models::dependency;
10-
use crate::models::{Badge, Category, Keyword, NewCrate, NewVersion, Rights, User};
10+
use crate::models::{
11+
Badge, Category, Keyword, NewCrate, NewVersion, insert_version_owner_action, Rights, User,
12+
VersionAction,
13+
};
14+
1115
use crate::render;
1216
use crate::util::{read_fill, read_le_u32};
1317
use crate::util::{CargoError, ChainError, Maximums};
@@ -143,6 +147,14 @@ pub fn publish(req: &mut dyn Request) -> CargoResult<Response> {
143147
)?
144148
.save(&conn, &new_crate.authors, &verified_email_address)?;
145149

150+
insert_version_owner_action(
151+
&conn,
152+
version.id,
153+
user.id,
154+
req.authentication_source()?.api_token_id(),
155+
VersionAction::Publish,
156+
)?;
157+
146158
// Link this new version to all dependencies
147159
let git_deps = dependency::add_dependencies(&conn, &new_crate.deps, version.id)?;
148160

src/controllers/version/yank.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use swirl::Job;
55
use super::version_and_crate;
66
use crate::controllers::prelude::*;
77
use crate::git;
8-
use crate::models::Rights;
8+
use crate::models::{insert_version_owner_action, Rights, VersionAction};
99
use crate::util::CargoError;
1010

1111
/// Handles the `DELETE /crates/:crate_id/:version/yank` route.
@@ -35,6 +35,14 @@ fn modify_yank(req: &mut dyn Request, yanked: bool) -> CargoResult<Response> {
3535
if user.rights(req.app(), &owners)? < Rights::Publish {
3636
return Err(human("must already be an owner to yank or unyank"));
3737
}
38+
let action = if yanked {
39+
VersionAction::Yank
40+
} else {
41+
VersionAction::Unyank
42+
};
43+
let api_token_id = req.authentication_source()?.api_token_id();
44+
45+
insert_version_owner_action(&conn, version.id, user.id, api_token_id, action)?;
3846

3947
git::yank(krate.name, version, yanked)
4048
.enqueue(&conn)

src/middleware/current_user.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ use diesel::prelude::*;
66
use crate::db::RequestTransaction;
77
use crate::util::errors::{std_error, CargoResult, ChainError, Unauthorized};
88

9+
use crate::models::ApiToken;
910
use crate::models::User;
1011
use crate::schema::users;
1112

1213
#[derive(Debug, Clone, Copy)]
1314
pub struct CurrentUser;
1415

15-
#[derive(Debug, Clone, Eq, PartialEq)]
16+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1617
pub enum AuthenticationSource {
1718
SessionCookie,
18-
ApiToken { auth_header: String },
19+
ApiToken { api_token_id: i32 },
1920
}
2021

2122
impl Middleware for CurrentUser {
@@ -43,10 +44,17 @@ impl Middleware for CurrentUser {
4344
// Otherwise, look for an `Authorization` header on the request
4445
// and try to find a user in the database with a matching API token
4546
let user_auth = if let Some(headers) = req.headers().find("Authorization") {
46-
let auth_header = headers[0].to_string();
47-
48-
User::find_by_api_token(&conn, &auth_header)
49-
.map(|user| (AuthenticationSource::ApiToken { auth_header }, user))
47+
ApiToken::find_by_api_token(&conn, headers[0])
48+
.and_then(|api_token| {
49+
User::find(&conn, api_token.user_id).map(|user| {
50+
(
51+
AuthenticationSource::ApiToken {
52+
api_token_id: api_token.id,
53+
},
54+
user,
55+
)
56+
})
57+
})
5058
.optional()
5159
.map_err(|e| Box::new(e) as Box<dyn Error + Send>)?
5260
} else {
@@ -85,3 +93,12 @@ impl<'a> RequestUser for dyn Request + 'a {
8593
.chain_error(|| Unauthorized)
8694
}
8795
}
96+
97+
impl AuthenticationSource {
98+
pub fn api_token_id(self) -> Option<i32> {
99+
match self {
100+
AuthenticationSource::SessionCookie => None,
101+
AuthenticationSource::ApiToken { api_token_id } => Some(api_token_id),
102+
}
103+
}
104+
}

src/models.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub use self::action::{VersionAction, VersionOwnerAction};
1+
pub use self::action::{insert_version_owner_action, VersionAction, VersionOwnerAction};
22
pub use self::badge::{Badge, CrateBadge, MaintenanceStatus};
33
pub use self::category::{Category, CrateCategory, NewCategory};
44
pub use self::crate_owner_invitation::{CrateOwnerInvitation, NewCrateOwnerInvitation};

src/models/action.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,89 @@
11
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;
210

311
use crate::models::{ApiToken, User, Version};
412
use crate::schema::*;
513

6-
#[derive(Debug, Clone, Copy)]
7-
#[repr(u32)]
14+
#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)]
15+
#[repr(i32)]
16+
#[sql_type = "Integer"]
817
pub enum VersionAction {
918
Publish = 0,
1019
Yank = 1,
1120
Unyank = 2,
1221
}
1322

23+
impl FromSql<Integer, Pg> for VersionAction {
24+
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
25+
match <i32 as FromSql<Integer, Pg>>::from_sql(bytes)? {
26+
0 => Ok(VersionAction::Publish),
27+
1 => Ok(VersionAction::Yank),
28+
2 => Ok(VersionAction::Unyank),
29+
n => Err(format!("unknown version action: {}", n).into()),
30+
}
31+
}
32+
}
33+
34+
impl ToSql<Integer, Pg> for VersionAction {
35+
fn to_sql<W: Write>(&self, out: &mut Output<'_, W, Pg>) -> serialize::Result {
36+
ToSql::<Integer, Pg>::to_sql(&(*self as i32), out)
37+
}
38+
}
39+
1440
#[derive(Debug, Clone, Copy, Queryable, Identifiable, Associations)]
1541
#[belongs_to(Version)]
1642
#[belongs_to(User, foreign_key = "owner_id")]
1743
#[belongs_to(ApiToken, foreign_key = "owner_token_id")]
1844
#[table_name = "version_owner_actions"]
1945
pub struct VersionOwnerAction {
2046
pub id: i32,
21-
pub version_id: i32,
22-
pub owner_id: i32,
23-
pub owner_token_id: i32,
47+
pub version_id: Option<i32>,
48+
pub owner_id: Option<i32>,
49+
pub owner_token_id: Option<i32>,
2450
pub action: VersionAction,
2551
pub time: NaiveDateTime,
2652
}
53+
54+
impl VersionOwnerAction {
55+
pub fn all(conn: &PgConnection) -> QueryResult<Vec<VersionOwnerAction>> {
56+
version_owner_actions::table.load(conn)
57+
}
58+
59+
pub fn by_version_id_and_action(
60+
conn: &PgConnection,
61+
_version_id: i32,
62+
_action: VersionAction,
63+
) -> QueryResult<Vec<VersionOwnerAction>> {
64+
use version_owner_actions::dsl::{action, version_id};
65+
66+
version_owner_actions::table
67+
.filter(version_id.eq(_version_id))
68+
.filter(action.eq(_action))
69+
.load(conn)
70+
}
71+
}
72+
73+
pub fn insert_version_owner_action(
74+
conn: &PgConnection,
75+
_version_id: i32,
76+
_owner_id: i32,
77+
_owner_token_id: Option<i32>,
78+
_action: VersionAction,
79+
) -> QueryResult<VersionOwnerAction> {
80+
use version_owner_actions::dsl::{version_id, owner_id, owner_token_id, action};
81+
82+
diesel::insert_into(version_owner_actions::table)
83+
.values((
84+
version_id.eq(_version_id),
85+
owner_id.eq(_owner_id),
86+
owner_token_id.eq(_owner_token_id),
87+
action.eq(_action)))
88+
.get_result(conn)
89+
}

src/models/token.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ impl ApiToken {
4646
last_used_at: self.last_used_at,
4747
}
4848
}
49+
50+
pub fn find_by_api_token(conn: &PgConnection, token_: &str) -> QueryResult<ApiToken> {
51+
use crate::schema::api_tokens::dsl::{api_tokens, last_used_at, revoked, token};
52+
use diesel::{dsl::now, update};
53+
54+
let tokens = api_tokens
55+
.filter(token.eq(token_))
56+
.filter(revoked.eq(false));
57+
58+
// If the database is in read only mode, we can't update last_used_at.
59+
// Try updating in a new transaction, if that fails, fall back to reading
60+
conn.transaction(|| {
61+
update(tokens)
62+
.set(last_used_at.eq(now.nullable()))
63+
.get_result::<ApiToken>(conn)
64+
})
65+
.or_else(|_| tokens.first(conn))
66+
}
4967
}
5068

5169
#[cfg(test)]

src/models/user.rs

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
use diesel::dsl::now;
21
use diesel::prelude::*;
32
use std::borrow::Cow;
43

54
use crate::app::App;
65
use crate::util::CargoResult;
76

8-
use crate::models::{Crate, CrateOwner, Email, NewEmail, Owner, OwnerKind, Rights};
7+
use crate::models::{ApiToken, Crate, CrateOwner, Email, NewEmail, Owner, OwnerKind, Rights};
98
use crate::schema::{crate_owners, emails, users};
109
use crate::views::{EncodablePrivateUser, EncodablePublicUser};
1110

@@ -105,27 +104,14 @@ impl<'a> NewUser<'a> {
105104
}
106105

107106
impl User {
108-
/// Queries the database for a user with a certain `api_token` value.
109-
pub fn find_by_api_token(conn: &PgConnection, token_: &str) -> QueryResult<User> {
110-
use crate::schema::api_tokens::dsl::{api_tokens, last_used_at, revoked, token, user_id};
111-
use diesel::update;
112-
113-
let tokens = api_tokens
114-
.filter(token.eq(token_))
115-
.filter(revoked.eq(false));
116-
117-
// If the database is in read only mode, we can't update last_used_at.
118-
// Try updating in a new transaction, if that fails, fall back to reading
119-
let user_id_ = conn
120-
.transaction(|| {
121-
update(tokens)
122-
.set(last_used_at.eq(now.nullable()))
123-
.returning(user_id)
124-
.get_result::<i32>(conn)
125-
})
126-
.or_else(|_| tokens.select(user_id).first(conn))?;
127-
128-
users::table.find(user_id_).first(conn)
107+
pub fn find(conn: &PgConnection, id: i32) -> QueryResult<User> {
108+
users::table.find(id).first(conn)
109+
}
110+
111+
pub fn find_by_api_token(conn: &PgConnection, token: &str) -> QueryResult<User> {
112+
let api_token = ApiToken::find_by_api_token(conn, token)?;
113+
114+
Self::find(conn, api_token.user_id)
129115
}
130116

131117
pub fn owning(krate: &Crate, conn: &PgConnection) -> CargoResult<Vec<Owner>> {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
[
2+
{
3+
"request": {
4+
"uri": "http://alexcrichton-test.s3.amazonaws.com/crates/fyk/fyk-1.0.0.crate",
5+
"method": "PUT",
6+
"headers": [
7+
[
8+
"host",
9+
"alexcrichton-test.s3.amazonaws.com"
10+
],
11+
[
12+
"date",
13+
"Fri, 15 Sep 2017 07:53:06 -0700"
14+
],
15+
[
16+
"accept",
17+
"*/*"
18+
],
19+
[
20+
"user-agent",
21+
"reqwest/0.9.1"
22+
],
23+
[
24+
"authorization",
25+
"AWS AKIAICL5IWUZYWWKA7JA:xCp3sUdUdmScjI6ct58zFv6BoGQ="
26+
],
27+
[
28+
"content-length",
29+
"35"
30+
],
31+
[
32+
"accept-encoding",
33+
"gzip"
34+
],
35+
[
36+
"content-type",
37+
"application/x-tar"
38+
]
39+
],
40+
"body": "H4sIAAAAAAAA/+3AAQEAAACCIP+vbkhQwKsBLq+17wAEAAA="
41+
},
42+
"response": {
43+
"status": 200,
44+
"headers": [
45+
[
46+
"ETag",
47+
"\"f9016ad360cebb4fe2e6e96e5949f022\""
48+
],
49+
[
50+
"x-amz-id-2",
51+
"FCKNKZUo5EeUNwVyhZ9P7ehfXoctqePzXx2RSE1VxoSX9rdfskkyAJUNHAF2AQojRon00LfTLPY="
52+
],
53+
[
54+
"x-amz-request-id",
55+
"3233F8227A852593"
56+
],
57+
[
58+
"Server",
59+
"AmazonS3"
60+
],
61+
[
62+
"content-length",
63+
"0"
64+
],
65+
[
66+
"date",
67+
"Fri, 15 Sep 2017 14:53:07 GMT"
68+
]
69+
],
70+
"body": ""
71+
}
72+
}
73+
]

0 commit comments

Comments
 (0)