Skip to content

Commit 9ed51dd

Browse files
committed
Use the database to generate API tokens
There's a ton of code which is setting this field in misleading ways, and it seems really odd to pass it to `find_or_insert` when it's completely ignored in the update case. We can just use a database default for this, and simplify everything a bit.
1 parent 35d2240 commit 9ed51dd

File tree

6 files changed

+92
-45
lines changed

6 files changed

+92
-45
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE users ALTER COLUMN api_token DROP DEFAULT;
2+
DROP FUNCTION random_string(int4);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CREATE FUNCTION random_string(int4) RETURNS text AS $$
2+
SELECT (array_to_string(array(
3+
SELECT substr(
4+
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
5+
floor(random() * 62)::int4 + 1,
6+
1
7+
) FROM generate_series(1, $1)
8+
), ''))
9+
$$ LANGUAGE SQL;
10+
11+
ALTER TABLE users ALTER COLUMN api_token SET DEFAULT random_string(32);

src/bin/update-downloads.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ mod test {
148148

149149
fn user(conn: &postgres::transaction::Transaction) -> User{
150150
User::find_or_insert(conn, 2, "login", None, None, None,
151-
"access_token", "api_token").unwrap()
151+
"access_token").unwrap()
152152
}
153153

154154
fn crate_downloads(tx: &postgres::transaction::Transaction, id: i32, expected: usize) {

src/tests/all.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ fn user(login: &str) -> User {
180180
email: None,
181181
name: None,
182182
avatar: None,
183-
gh_access_token: User::new_api_token(), // just randomize it
184-
api_token: User::new_api_token(),
183+
gh_access_token: "some random token".into(),
184+
api_token: "some random token".into(),
185185
}
186186
}
187187

@@ -209,12 +209,15 @@ fn mock_user(req: &mut Request, u: User) -> User {
209209
u.email.as_ref().map(|s| &s[..]),
210210
u.name.as_ref().map(|s| &s[..]),
211211
u.avatar.as_ref().map(|s| &s[..]),
212-
&u.gh_access_token,
213-
&u.api_token).unwrap();
214-
req.mut_extensions().insert(u.clone());
212+
&u.gh_access_token).unwrap();
213+
sign_in_as(req, &u);
215214
return u;
216215
}
217216

217+
fn sign_in_as(req: &mut Request, user: &User) {
218+
req.mut_extensions().insert(user.clone());
219+
}
220+
218221
fn mock_crate(req: &mut Request, krate: Crate) -> (Crate, Version) {
219222
mock_crate_vers(req, krate, &semver::Version::parse("1.0.0").unwrap())
220223
}

src/tests/user.rs

+13-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use conduit::{Handler, Request, Method};
1+
use conduit::{Handler, Method};
22

33
use cargo_registry::Model;
44
use cargo_registry::krate::EncodableCrate;
@@ -37,20 +37,20 @@ fn user_insert() {
3737
let conn = t!(app.database.get());
3838
let tx = t!(conn.transaction());
3939

40-
let user = t!(User::find_or_insert(&tx, 1, "foo", None, None, None, "bar", "baz"));
41-
assert_eq!(t!(User::find_by_api_token(&tx, "baz")), user);
40+
let user = t!(User::find_or_insert(&tx, 1, "foo", None, None, None, "bar"));
41+
assert_eq!(t!(User::find_by_api_token(&tx, &user.api_token)), user);
4242
assert_eq!(t!(User::find(&tx, user.id)), user);
4343

4444
assert_eq!(t!(User::find_or_insert(&tx, 1, "foo", None, None, None,
45-
"bar", "api")), user);
45+
"bar")), user);
4646
let user2 = t!(User::find_or_insert(&tx, 1, "foo", None, None, None,
47-
"baz", "api"));
47+
"baz"));
4848
assert!(user != user2);
4949
assert_eq!(user.id, user2.id);
5050
assert_eq!(user2.gh_access_token, "baz");
5151

5252
let user3 = t!(User::find_or_insert(&tx, 1, "bar", None, None, None,
53-
"baz", "api"));
53+
"baz"));
5454
assert!(user != user3);
5555
assert_eq!(user.id, user3.id);
5656
assert_eq!(user3.gh_login, "bar");
@@ -63,8 +63,7 @@ fn me() {
6363
let response = t_resp!(middle.call(&mut req));
6464
assert_eq!(response.status.0, 403);
6565

66-
let user = ::user("foo");
67-
::mock_user(&mut req, user.clone());
66+
let user = ::mock_user(&mut req, ::user("foo"));
6867
let mut response = ok_resp!(middle.call(&mut req));
6968
let json: MeResponse = ::json(&mut response);
7069
assert_eq!(json.user.email, user.email);
@@ -77,8 +76,8 @@ fn show() {
7776
{
7877
let conn = t!(app.diesel_database.get());
7978

80-
t!(NewUser::new(1, "foo", Some("foo@bar.com"), None, None, "bar", "baz").create_or_update(&conn));
81-
t!(NewUser::new(2, "bar", Some("bar@baz.com"), None, None, "bar", "baz").create_or_update(&conn));
79+
t!(NewUser::new(1, "foo", Some("foo@bar.com"), None, None, "bar").create_or_update(&conn));
80+
t!(NewUser::new(2, "bar", Some("bar@baz.com"), None, None, "bar").create_or_update(&conn));
8281
}
8382

8483
let mut req = ::req(app.clone(), Method::Get, "/api/v1/users/foo");
@@ -97,16 +96,12 @@ fn show() {
9796
fn reset_token() {
9897
let (_b, app, middle) = ::app();
9998
let mut req = ::req(app, Method::Put, "/me/reset_token");
100-
let user = {
101-
let tx = RequestTransaction::tx(&mut req as &mut Request);
102-
User::find_or_insert(tx.unwrap(), 1, "foo", None,
103-
None, None, "bar", "baz").unwrap()
104-
};
105-
::mock_user(&mut req, user.clone());
99+
let user = User::find_or_insert(req.tx().unwrap(), 1, "foo", None,
100+
None, None, "bar").unwrap();
101+
::sign_in_as(&mut req, &user);
106102
ok_resp!(middle.call(&mut req));
107103

108-
let tx = RequestTransaction::tx(&mut req as &mut Request);
109-
let u2 = User::find(tx.unwrap(), user.id).unwrap();
104+
let u2 = User::find(req.tx().unwrap(), user.id).unwrap();
110105
assert!(u2.api_token != user.api_token);
111106
}
112107

src/user/mod.rs

+57-21
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ pub struct NewUser<'a> {
4444
pub name: Option<&'a str>,
4545
pub gh_avatar: Option<&'a str>,
4646
pub gh_access_token: &'a str,
47-
pub api_token: &'a str,
4847
}
4948

5049
impl<'a> NewUser<'a> {
@@ -53,16 +52,14 @@ impl<'a> NewUser<'a> {
5352
email: Option<&'a str>,
5453
name: Option<&'a str>,
5554
gh_avatar: Option<&'a str>,
56-
gh_access_token: &'a str,
57-
api_token: &'a str) -> Self {
55+
gh_access_token: &'a str) -> Self {
5856
NewUser {
5957
gh_id: gh_id,
6058
gh_login: gh_login,
6159
email: email,
6260
name: name,
6361
gh_avatar: gh_avatar,
6462
gh_access_token: gh_access_token,
65-
api_token: api_token,
6663
}
6764
}
6865

@@ -134,8 +131,7 @@ impl User {
134131
email: Option<&str>,
135132
name: Option<&str>,
136133
avatar: Option<&str>,
137-
access_token: &str,
138-
api_token: &str) -> CargoResult<User> {
134+
access_token: &str) -> CargoResult<User> {
139135
// TODO: this is racy, but it looks like any other solution is...
140136
// interesting! For now just do the racy thing which will report
141137
// more errors than it needs to.
@@ -159,13 +155,12 @@ impl User {
159155
None => {}
160156
}
161157
let stmt = conn.prepare("INSERT INTO users
162-
(email, gh_access_token, api_token,
158+
(email, gh_access_token,
163159
gh_login, name, gh_avatar, gh_id)
164-
VALUES ($1, $2, $3, $4, $5, $6, $7)
160+
VALUES ($1, $2, $3, $4, $5, $6)
165161
RETURNING *")?;
166162
let rows = stmt.query(&[&email,
167163
&access_token,
168-
&api_token,
169164
&login,
170165
&name,
171166
&avatar,
@@ -175,11 +170,6 @@ impl User {
175170
})?))
176171
}
177172

178-
/// Generates a new crates.io API token.
179-
pub fn new_api_token() -> String {
180-
thread_rng().gen_ascii_chars().take(32).collect()
181-
}
182-
183173
/// Converts this `User` model into an `EncodableUser` for JSON serialization.
184174
pub fn encodable(self) -> EncodableUser {
185175
let User { id, email, api_token: _, gh_access_token: _,
@@ -300,8 +290,6 @@ pub fn github_access_token(req: &mut Request) -> CargoResult<Response> {
300290
let (handle, resp) = http::github(req.app(), "/user", &token)?;
301291
let ghuser: GithubUser = http::parse_github_response(handle, resp)?;
302292

303-
// Into the database!
304-
let api_token = User::new_api_token();
305293
let user = User::find_or_insert(req.tx()?,
306294
ghuser.id,
307295
&ghuser.login,
@@ -311,8 +299,7 @@ pub fn github_access_token(req: &mut Request) -> CargoResult<Response> {
311299
.map(|s| &s[..]),
312300
ghuser.avatar_url.as_ref()
313301
.map(|s| &s[..]),
314-
&token.access_token,
315-
&api_token)?;
302+
&token.access_token)?;
316303
req.session().insert("user_id".to_string(), user.id.to_string());
317304
req.mut_extensions().insert(user);
318305
me(req)
@@ -328,10 +315,12 @@ pub fn logout(req: &mut Request) -> CargoResult<Response> {
328315
pub fn reset_token(req: &mut Request) -> CargoResult<Response> {
329316
let user = req.user()?;
330317

331-
let token = User::new_api_token();
332318
let conn = req.tx()?;
333-
conn.execute("UPDATE users SET api_token = $1 WHERE id = $2",
334-
&[&token, &user.id])?;
319+
let rows = conn.query("UPDATE users SET api_token = DEFAULT \
320+
WHERE id = $1 RETURNING api_token", &[&user.id])?;
321+
let token = rows.iter().next()
322+
.map(|r| r.get("api_token"))
323+
.chain_error(|| NotFound)?;
335324

336325
#[derive(RustcEncodable)]
337326
struct R { api_token: String }
@@ -424,3 +413,50 @@ pub fn updates(req: &mut Request) -> CargoResult<Response> {
424413
struct Meta { more: bool }
425414
Ok(req.json(&R{ versions: versions, crates: crates, meta: Meta { more: more } }))
426415
}
416+
417+
#[cfg(test)]
418+
mod tests {
419+
use super::*;
420+
use diesel::pg::PgConnection;
421+
use dotenv::dotenv;
422+
use std::env;
423+
424+
fn connection() -> PgConnection {
425+
let _ = dotenv();
426+
let database_url = env::var("TEST_DATABASE_URL")
427+
.expect("TEST_DATABASE_URL must be set to run tests");
428+
let conn = PgConnection::establish(&database_url).unwrap();
429+
conn.begin_test_transaction().unwrap();
430+
conn
431+
}
432+
433+
#[test]
434+
fn new_users_have_different_api_tokens() {
435+
let conn = connection();
436+
let user1 = NewUser::new(1, "foo", None, None, None, "foo")
437+
.create_or_update(&conn).unwrap();
438+
let user2 = NewUser::new(2, "bar", None, None, None, "bar")
439+
.create_or_update(&conn).unwrap();
440+
441+
assert_ne!(user1.id, user2.id);
442+
assert_ne!(user1.api_token, user2.api_token);
443+
assert_eq!(32, user1.api_token.len());
444+
}
445+
446+
#[test]
447+
fn updating_existing_user_doesnt_change_api_token() {
448+
let conn = connection();
449+
let user_after_insert = NewUser::new(1, "foo", None, None, None, "foo")
450+
.create_or_update(&conn).unwrap();
451+
let original_token = user_after_insert.api_token;
452+
NewUser::new(1, "bar", None, None, None, "bar_token")
453+
.create_or_update(&conn).unwrap();
454+
let mut users = users::table.load::<User>(&conn).unwrap();
455+
assert_eq!(1, users.len());
456+
let user = users.pop().unwrap();
457+
458+
assert_eq!("bar", user.gh_login);
459+
assert_eq!("bar_token", user.gh_access_token);
460+
assert_eq!(original_token, user.api_token);
461+
}
462+
}

0 commit comments

Comments
 (0)