diff --git a/migrations/postgres/2018-10-21-163227_create_api_token/down.sql b/migrations/postgres/2018-10-21-163227_create_api_token/down.sql new file mode 100644 index 00000000..e71f1478 --- /dev/null +++ b/migrations/postgres/2018-10-21-163227_create_api_token/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE api_tokens; diff --git a/migrations/postgres/2018-10-21-163227_create_api_token/up.sql b/migrations/postgres/2018-10-21-163227_create_api_token/up.sql new file mode 100644 index 00000000..ecf9f512 --- /dev/null +++ b/migrations/postgres/2018-10-21-163227_create_api_token/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE api_tokens ( + id SERIAL PRIMARY KEY, + creation_date TIMESTAMP NOT NULL DEFAULT now(), + value TEXT NOT NULL, + scopes TEXT NOT NULL, + app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE +) diff --git a/migrations/sqlite/2018-10-21-163241_create_api_token/down.sql b/migrations/sqlite/2018-10-21-163241_create_api_token/down.sql new file mode 100644 index 00000000..e71f1478 --- /dev/null +++ b/migrations/sqlite/2018-10-21-163241_create_api_token/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE api_tokens; diff --git a/migrations/sqlite/2018-10-21-163241_create_api_token/up.sql b/migrations/sqlite/2018-10-21-163241_create_api_token/up.sql new file mode 100644 index 00000000..7d6f6cf0 --- /dev/null +++ b/migrations/sqlite/2018-10-21-163241_create_api_token/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE api_tokens ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + value TEXT NOT NULL, + scopes TEXT NOT NULL, + app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE +) diff --git a/plume-common/src/utils.rs b/plume-common/src/utils.rs index e17399d4..9ac5cec2 100644 --- a/plume-common/src/utils.rs +++ b/plume-common/src/utils.rs @@ -1,5 +1,6 @@ use gettextrs::gettext; use heck::CamelCase; +use openssl::rand::rand_bytes; use pulldown_cmark::{Event, Parser, Options, Tag, html}; use rocket::{ http::uri::Uri, @@ -7,6 +8,13 @@ use rocket::{ }; use std::collections::HashSet; +/// Generates an hexadecimal representation of 32 bytes of random data +pub fn random_hex() -> String { + let mut bytes = [0; 32]; + rand_bytes(&mut bytes).expect("Error while generating client id"); + bytes.into_iter().fold(String::new(), |res, byte| format!("{}{:x}", res, byte)) +} + /// Remove non alphanumeric characters and CamelCase a string pub fn make_actor_id(name: String) -> String { name.as_str() diff --git a/plume-models/src/api_tokens.rs b/plume-models/src/api_tokens.rs new file mode 100644 index 00000000..6245bf43 --- /dev/null +++ b/plume-models/src/api_tokens.rs @@ -0,0 +1,40 @@ +use chrono::NaiveDateTime; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; + +use schema::api_tokens; + +#[derive(Clone, Queryable)] +pub struct ApiToken { + pub id: i32, + pub creation_date: NaiveDateTime, + pub value: String, + + /// Scopes, separated by + + /// Global scopes are read and write + /// and both can be limited to an endpoint by affixing them with :ENDPOINT + /// + /// Examples : + /// + /// read + /// read+write + /// read:posts + /// read:posts+write:posts + pub scopes: String, + pub app_id: i32, + pub user_id: i32, +} + +#[derive(Insertable)] +#[table_name = "api_tokens"] +pub struct NewApiToken { + pub value: String, + pub scopes: String, + pub app_id: i32, + pub user_id: i32, +} + +impl ApiToken { + get!(api_tokens); + insert!(api_tokens, NewApiToken); + find_by!(api_tokens, find_by_value, value as String); +} diff --git a/plume-models/src/apps.rs b/plume-models/src/apps.rs index a636c410..07612c3d 100755 --- a/plume-models/src/apps.rs +++ b/plume-models/src/apps.rs @@ -1,9 +1,9 @@ use canapi::{Error, Provider}; use chrono::NaiveDateTime; use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods}; -use openssl::rand::rand_bytes; use plume_api::apps::AppEndpoint; +use plume_common::utils::random_hex; use Connection; use schema::apps; @@ -14,7 +14,7 @@ pub struct App { pub client_id: String, pub client_secret: String, pub redirect_uri: Option, - pub website: Option, + pub website: Option, pub creation_date: NaiveDateTime, } @@ -25,7 +25,7 @@ pub struct NewApp { pub client_id: String, pub client_secret: String, pub redirect_uri: Option, - pub website: Option, + pub website: Option, } impl Provider for App { @@ -40,13 +40,9 @@ impl Provider for App { } fn create(conn: &Connection, data: AppEndpoint) -> Result { - let mut id = [0; 32]; - rand_bytes(&mut id).expect("Error while generating client id"); - let client_id = id.into_iter().fold(String::new(), |res, byte| format!("{}{:x}", res, byte)); - - let mut secret = [0; 32]; - rand_bytes(&mut secret).expect("Error while generating client secret"); - let client_secret = secret.into_iter().fold(String::new(), |res, byte| format!("{}{:x}", res, byte)); + let client_id = random_hex(); + + let client_secret = random_hex(); let app = App::insert(conn, NewApp { name: data.name.expect("App::create: name is required"), client_id: client_id, @@ -68,7 +64,7 @@ impl Provider for App { fn update(conn: &Connection, id: i32, new_data: AppEndpoint) -> Result { unimplemented!() } - + fn delete(conn: &Connection, id: i32) { unimplemented!() } @@ -77,4 +73,5 @@ impl Provider for App { impl App { get!(apps); insert!(apps, NewApp); -} + find_by!(apps, find_by_client_id, client_id as String); +} diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index deab3cd5..525edc26 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -214,6 +214,7 @@ pub fn ap_url(url: String) -> String { } pub mod admin; +pub mod api_tokens; pub mod apps; pub mod blog_authors; pub mod blogs; diff --git a/plume-models/src/schema.rs b/plume-models/src/schema.rs index 582fb464..5be393f3 100644 --- a/plume-models/src/schema.rs +++ b/plume-models/src/schema.rs @@ -1,3 +1,14 @@ +table! { + api_tokens (id) { + id -> Int4, + creation_date -> Timestamp, + value -> Text, + scopes -> Text, + app_id -> Int4, + user_id -> Int4, + } +} + table! { apps (id) { id -> Int4, @@ -184,6 +195,8 @@ table! { } } +joinable!(api_tokens -> apps (app_id)); +joinable!(api_tokens -> users (user_id)); joinable!(blog_authors -> blogs (blog_id)); joinable!(blog_authors -> users (author_id)); joinable!(blogs -> instances (instance_id)); @@ -204,6 +217,7 @@ joinable!(tags -> posts (post_id)); joinable!(users -> instances (instance_id)); allow_tables_to_appear_in_same_query!( + api_tokens, apps, blog_authors, blogs, diff --git a/src/api/mod.rs b/src/api/mod.rs index 1e1d1680..11353f4d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,2 +1,54 @@ +use rocket_contrib::Json; +use serde_json; + +use plume_common::utils::random_hex; +use plume_models::{ + apps::App, + api_tokens::*, + db_conn::DbConn, + users::User, +}; + +#[derive(FromForm)] +struct OAuthRequest { + client_id: String, + client_secret: String, + password: String, + username: String, + scopes: String, +} + +#[get("/oauth2?")] +fn oauth(query: OAuthRequest, conn: DbConn) -> Json { + let app = App::find_by_client_id(&*conn, query.client_id).expect("OAuth request from unknown client"); + if app.client_secret == query.client_secret { + if let Some(user) = User::find_local(&*conn, query.username) { + if user.auth(query.password) { + let token = ApiToken::insert(&*conn, NewApiToken { + app_id: app.id, + user_id: user.id, + value: random_hex(), + scopes: query.scopes, + }); + Json(json!({ + "token": token.value + })) + } else { + Json(json!({ + "error": "Wrong password" + })) + } + } else { + Json(json!({ + "error": "Unknown user" + })) + } + } else { + Json(json!({ + "error": "Invalid client_secret" + })) + } +} + pub mod apps; pub mod posts; diff --git a/src/main.rs b/src/main.rs index a2395a8e..cfa8c87b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,6 +156,8 @@ fn main() { routes::errors::csrf_violation ]) .mount("/api/v1", routes![ + api::oauth, + api::apps::create, api::posts::get,