Skip to content

Use the database to generate API tokens #607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE users ALTER COLUMN api_token DROP DEFAULT;
DROP FUNCTION random_string(int4);
11 changes: 11 additions & 0 deletions migrations/20170309122510_generate_api_token_with_sql/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE FUNCTION random_string(int4) RETURNS text AS $$
SELECT (array_to_string(array(
SELECT substr(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
floor(random() * 62)::int4 + 1,
1
) FROM generate_series(1, $1)
), ''))
$$ LANGUAGE SQL;

ALTER TABLE users ALTER COLUMN api_token SET DEFAULT random_string(32);
2 changes: 1 addition & 1 deletion src/bin/update-downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ mod test {

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

fn crate_downloads(tx: &postgres::transaction::Transaction, id: i32, expected: usize) {
Expand Down
13 changes: 8 additions & 5 deletions src/tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ fn user(login: &str) -> User {
email: None,
name: None,
avatar: None,
gh_access_token: User::new_api_token(), // just randomize it
api_token: User::new_api_token(),
gh_access_token: "some random token".into(),
api_token: "some random token".into(),
}
}

Expand Down Expand Up @@ -209,12 +209,15 @@ fn mock_user(req: &mut Request, u: User) -> User {
u.email.as_ref().map(|s| &s[..]),
u.name.as_ref().map(|s| &s[..]),
u.avatar.as_ref().map(|s| &s[..]),
&u.gh_access_token,
&u.api_token).unwrap();
req.mut_extensions().insert(u.clone());
&u.gh_access_token).unwrap();
sign_in_as(req, &u);
return u;
}

fn sign_in_as(req: &mut Request, user: &User) {
req.mut_extensions().insert(user.clone());
}

fn mock_crate(req: &mut Request, krate: Crate) -> (Crate, Version) {
mock_crate_vers(req, krate, &semver::Version::parse("1.0.0").unwrap())
}
Expand Down
31 changes: 13 additions & 18 deletions src/tests/user.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use conduit::{Handler, Request, Method};
use conduit::{Handler, Method};

use cargo_registry::Model;
use cargo_registry::krate::EncodableCrate;
Expand Down Expand Up @@ -37,20 +37,20 @@ fn user_insert() {
let conn = t!(app.database.get());
let tx = t!(conn.transaction());

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

assert_eq!(t!(User::find_or_insert(&tx, 1, "foo", None, None, None,
"bar", "api")), user);
"bar")), user);
let user2 = t!(User::find_or_insert(&tx, 1, "foo", None, None, None,
"baz", "api"));
"baz"));
assert!(user != user2);
assert_eq!(user.id, user2.id);
assert_eq!(user2.gh_access_token, "baz");

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

let user = ::user("foo");
::mock_user(&mut req, user.clone());
let user = ::mock_user(&mut req, ::user("foo"));
let mut response = ok_resp!(middle.call(&mut req));
let json: MeResponse = ::json(&mut response);
assert_eq!(json.user.email, user.email);
Expand All @@ -77,8 +76,8 @@ fn show() {
{
let conn = t!(app.diesel_database.get());

t!(NewUser::new(1, "foo", Some("[email protected]"), None, None, "bar", "baz").create_or_update(&conn));
t!(NewUser::new(2, "bar", Some("[email protected]"), None, None, "bar", "baz").create_or_update(&conn));
t!(NewUser::new(1, "foo", Some("[email protected]"), None, None, "bar").create_or_update(&conn));
t!(NewUser::new(2, "bar", Some("[email protected]"), None, None, "bar").create_or_update(&conn));
}

let mut req = ::req(app.clone(), Method::Get, "/api/v1/users/foo");
Expand All @@ -97,16 +96,12 @@ fn show() {
fn reset_token() {
let (_b, app, middle) = ::app();
let mut req = ::req(app, Method::Put, "/me/reset_token");
let user = {
let tx = RequestTransaction::tx(&mut req as &mut Request);
User::find_or_insert(tx.unwrap(), 1, "foo", None,
None, None, "bar", "baz").unwrap()
};
::mock_user(&mut req, user.clone());
let user = User::find_or_insert(req.tx().unwrap(), 1, "foo", None,
None, None, "bar").unwrap();
::sign_in_as(&mut req, &user);
ok_resp!(middle.call(&mut req));

let tx = RequestTransaction::tx(&mut req as &mut Request);
let u2 = User::find(tx.unwrap(), user.id).unwrap();
let u2 = User::find(req.tx().unwrap(), user.id).unwrap();
assert!(u2.api_token != user.api_token);
}

Expand Down
78 changes: 57 additions & 21 deletions src/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ pub struct NewUser<'a> {
pub name: Option<&'a str>,
pub gh_avatar: Option<&'a str>,
pub gh_access_token: &'a str,
pub api_token: &'a str,
}

impl<'a> NewUser<'a> {
Expand All @@ -53,16 +52,14 @@ impl<'a> NewUser<'a> {
email: Option<&'a str>,
name: Option<&'a str>,
gh_avatar: Option<&'a str>,
gh_access_token: &'a str,
api_token: &'a str) -> Self {
gh_access_token: &'a str) -> Self {
NewUser {
gh_id: gh_id,
gh_login: gh_login,
email: email,
name: name,
gh_avatar: gh_avatar,
gh_access_token: gh_access_token,
api_token: api_token,
}
}

Expand Down Expand Up @@ -134,8 +131,7 @@ impl User {
email: Option<&str>,
name: Option<&str>,
avatar: Option<&str>,
access_token: &str,
api_token: &str) -> CargoResult<User> {
access_token: &str) -> CargoResult<User> {
// TODO: this is racy, but it looks like any other solution is...
// interesting! For now just do the racy thing which will report
// more errors than it needs to.
Expand All @@ -159,13 +155,12 @@ impl User {
None => {}
}
let stmt = conn.prepare("INSERT INTO users
(email, gh_access_token, api_token,
(email, gh_access_token,
gh_login, name, gh_avatar, gh_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *")?;
let rows = stmt.query(&[&email,
&access_token,
&api_token,
&login,
&name,
&avatar,
Expand All @@ -175,11 +170,6 @@ impl User {
})?))
}

/// Generates a new crates.io API token.
pub fn new_api_token() -> String {
thread_rng().gen_ascii_chars().take(32).collect()
}

/// Converts this `User` model into an `EncodableUser` for JSON serialization.
pub fn encodable(self) -> EncodableUser {
let User { id, email, api_token: _, gh_access_token: _,
Expand Down Expand Up @@ -300,8 +290,6 @@ pub fn github_access_token(req: &mut Request) -> CargoResult<Response> {
let (handle, resp) = http::github(req.app(), "/user", &token)?;
let ghuser: GithubUser = http::parse_github_response(handle, resp)?;

// Into the database!
let api_token = User::new_api_token();
let user = User::find_or_insert(req.tx()?,
ghuser.id,
&ghuser.login,
Expand All @@ -311,8 +299,7 @@ pub fn github_access_token(req: &mut Request) -> CargoResult<Response> {
.map(|s| &s[..]),
ghuser.avatar_url.as_ref()
.map(|s| &s[..]),
&token.access_token,
&api_token)?;
&token.access_token)?;
req.session().insert("user_id".to_string(), user.id.to_string());
req.mut_extensions().insert(user);
me(req)
Expand All @@ -328,10 +315,12 @@ pub fn logout(req: &mut Request) -> CargoResult<Response> {
pub fn reset_token(req: &mut Request) -> CargoResult<Response> {
let user = req.user()?;

let token = User::new_api_token();
let conn = req.tx()?;
conn.execute("UPDATE users SET api_token = $1 WHERE id = $2",
&[&token, &user.id])?;
let rows = conn.query("UPDATE users SET api_token = DEFAULT \
WHERE id = $1 RETURNING api_token", &[&user.id])?;
let token = rows.iter().next()
.map(|r| r.get("api_token"))
.chain_error(|| NotFound)?;

#[derive(RustcEncodable)]
struct R { api_token: String }
Expand Down Expand Up @@ -424,3 +413,50 @@ pub fn updates(req: &mut Request) -> CargoResult<Response> {
struct Meta { more: bool }
Ok(req.json(&R{ versions: versions, crates: crates, meta: Meta { more: more } }))
}

#[cfg(test)]
mod tests {
use super::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;

fn connection() -> PgConnection {
let _ = dotenv();
let database_url = env::var("TEST_DATABASE_URL")
.expect("TEST_DATABASE_URL must be set to run tests");
let conn = PgConnection::establish(&database_url).unwrap();
conn.begin_test_transaction().unwrap();
conn
}

#[test]
fn new_users_have_different_api_tokens() {
let conn = connection();
let user1 = NewUser::new(1, "foo", None, None, None, "foo")
.create_or_update(&conn).unwrap();
let user2 = NewUser::new(2, "bar", None, None, None, "bar")
.create_or_update(&conn).unwrap();

assert_ne!(user1.id, user2.id);
assert_ne!(user1.api_token, user2.api_token);
assert_eq!(32, user1.api_token.len());
}

#[test]
fn updating_existing_user_doesnt_change_api_token() {
let conn = connection();
let user_after_insert = NewUser::new(1, "foo", None, None, None, "foo")
.create_or_update(&conn).unwrap();
let original_token = user_after_insert.api_token;
NewUser::new(1, "bar", None, None, None, "bar_token")
.create_or_update(&conn).unwrap();
let mut users = users::table.load::<User>(&conn).unwrap();
assert_eq!(1, users.len());
let user = users.pop().unwrap();

assert_eq!("bar", user.gh_login);
assert_eq!("bar_token", user.gh_access_token);
assert_eq!(original_token, user.api_token);
}
}