Skip to content
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

API Authentication #285

Merged
merged 13 commits into from
Oct 30, 2018
Merged
Show file tree
Hide file tree
Changes from 10 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
33 changes: 33 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
# API documentation

## Getting an API token

To get access to the API, you should register your app and obtain a
token. To do so, use the `/api/v1/apps` API (accessible without a token) to create
a new app. Store the result somewhere for future use.

Then send a request to `/api/v1/oauth2`, with the following GET parameters:

- `client_id`, your client ID.
- `client_secret`, your client secret.
- `scopes`, the scopes you want to access. They are separated by `+`, and can either
be `read` (global read), `write` (global write), `read:SCOPE` (read only in `SCOPE`),
or `write:SCOPE` (write only in `SCOPE`).
- `username` the username (not the email, display name nor the fully qualified name) of the
user using your app.
- `password`, the password of the user.

Plume will respond with something similar to:

```json
{
"token": "<YOUR TOKEN HERE>"
}
```

To authenticate your requests you should put this token in the `Authorization` header:

```
Authorization: Bearer <YOUR TOKEN HERE>
```

<script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>

<div id="api"></div>
Expand Down
19 changes: 19 additions & 0 deletions docs/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ servers:
description: Demo instance

paths:
/apps:
post:
description:
Registers an application.
/posts/{id}:
get:
description:
Expand All @@ -27,6 +31,21 @@ paths:
List posts.

definitions:
App:
type: "object"
properties:
name:
type: "string"
example: "My app"
website:
type: "string"
example: "https://my.app"
client_id:
type: "string"
example: "My app"
client_secret:
type: "string"
example: "My app"
Post:
type: "object"
properties:
Expand Down
2 changes: 2 additions & 0 deletions migrations/postgres/2018-10-19-165407_create_apps/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE apps;
10 changes: 10 additions & 0 deletions migrations/postgres/2018-10-19-165407_create_apps/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Your SQL goes here
CREATE TABLE apps (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
client_id TEXT NOT NULL,
client_secret TEXT NOT NULL,
redirect_uri TEXT,
website TEXT,
creation_date TIMESTAMP NOT NULL DEFAULT now()
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE api_tokens;
9 changes: 9 additions & 0 deletions migrations/postgres/2018-10-21-163227_create_api_token/up.sql
Original file line number Diff line number Diff line change
@@ -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
)
2 changes: 2 additions & 0 deletions migrations/sqlite/2018-10-19-165450_create_apps/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE apps;
10 changes: 10 additions & 0 deletions migrations/sqlite/2018-10-19-165450_create_apps/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Your SQL goes here
CREATE TABLE apps (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '',
client_id TEXT NOT NULL,
client_secret TEXT NOT NULL,
redirect_uri TEXT,
website TEXT,
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
2 changes: 2 additions & 0 deletions migrations/sqlite/2018-10-21-163241_create_api_token/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE api_tokens;
9 changes: 9 additions & 0 deletions migrations/sqlite/2018-10-21-163241_create_api_token/up.sql
Original file line number Diff line number Diff line change
@@ -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
)
13 changes: 13 additions & 0 deletions plume-api/src/apps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use canapi::Endpoint;

#[derive(Clone, Default, Serialize, Deserialize)]
pub struct AppEndpoint {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feel strange to be at the same time data received from Post (with id, client_id and client_secret ignored, as they must be generated by the server) and data returned by the api (with those same field used, and most likely different than what was originally posted if they where). It should either be 2 different struct or at least a struct with FromForm custom-implemented to ensure that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use FromForm in plume-api, or we would loose all the benefits of canapi.

But I think I may add a Server/Client/Both wrapper type to specify when a field is required and make it easier to check if something has been forgotten.

Or maybe canapi is just a bad idea and we should drop it... 🤔

pub id: Option<i32>,
pub name: String,
pub website: Option<String>,
pub redirect_uri: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
}

api!("/api/v1/apps" => AppEndpoint);
1 change: 1 addition & 0 deletions plume-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ macro_rules! api {
};
}

pub mod apps;
pub mod posts;
8 changes: 8 additions & 0 deletions plume-common/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
use gettextrs::gettext;
use heck::CamelCase;
use openssl::rand::rand_bytes;
use pulldown_cmark::{Event, Parser, Options, Tag, html};
use rocket::{
http::uri::Uri,
response::{Redirect, Flash}
};

/// 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()
Expand Down
88 changes: 88 additions & 0 deletions plume-models/src/api_tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::{
Outcome,
http::Status,
request::{self, FromRequest, Request}
};

use db_conn::DbConn;
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);

pub fn can(&self, what: &'static str, scope: &'static str) -> bool {
let full_scope = what.to_owned() + ":" + scope;
for s in self.scopes.split('+') {
if s == what || s == full_scope {
return true
}
}
false
}

pub fn can_read(&self, what: &'static str) -> bool {
self.can("read", what)
}
trinity-1686a marked this conversation as resolved.
Show resolved Hide resolved

pub fn can_write(&self, what: &'static str) -> bool {
self.can("write", what)
}
trinity-1686a marked this conversation as resolved.
Show resolved Hide resolved
}

impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
type Error = ();

fn from_request(request: &'a Request<'r>) -> request::Outcome<ApiToken, ()> {
let headers: Vec<_> = request.headers().get("Authorization").collect();
if headers.len() != 1 {
return Outcome::Failure((Status::BadRequest, ()));
}

let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header.next().expect("Expect a token type");
let val = parsed_header.next().expect("Expect a token value");

if auth_type == "Bearer" {
let conn = request.guard::<DbConn>().expect("Couldn't connect to DB");
if let Some(token) = ApiToken::find_by_value(&*conn, val.to_string()) {
return Outcome::Success(token);
}
}

return Outcome::Forward(());
}
}
77 changes: 77 additions & 0 deletions plume-models/src/apps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use canapi::{Error, Provider};
igalic marked this conversation as resolved.
Show resolved Hide resolved
use chrono::NaiveDateTime;
use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods};

use plume_api::apps::AppEndpoint;
use plume_common::utils::random_hex;
use Connection;
use schema::apps;

#[derive(Clone, Queryable)]
pub struct App {
pub id: i32,
pub name: String,
pub client_id: String,
pub client_secret: String,
pub redirect_uri: Option<String>,
pub website: Option<String>,
pub creation_date: NaiveDateTime,
}

#[derive(Insertable)]
#[table_name= "apps"]
pub struct NewApp {
pub name: String,
pub client_id: String,
pub client_secret: String,
pub redirect_uri: Option<String>,
pub website: Option<String>,
}

impl Provider<Connection> for App {
type Data = AppEndpoint;

fn get(conn: &Connection, id: i32) -> Result<AppEndpoint, Error> {
unimplemented!()
}

fn list(conn: &Connection, query: AppEndpoint) -> Vec<AppEndpoint> {
unimplemented!()
}

fn create(conn: &Connection, data: AppEndpoint) -> Result<AppEndpoint, Error> {
let client_id = random_hex();

let client_secret = random_hex();
let app = App::insert(conn, NewApp {
name: data.name,
client_id: client_id,
client_secret: client_secret,
redirect_uri: data.redirect_uri,
website: data.website,
});

Ok(AppEndpoint {
id: Some(app.id),
name: app.name,
client_id: Some(app.client_id),
client_secret: Some(app.client_secret),
redirect_uri: app.redirect_uri,
website: app.website,
})
}

fn update(conn: &Connection, id: i32, new_data: AppEndpoint) -> Result<AppEndpoint, Error> {
unimplemented!()
}

fn delete(conn: &Connection, id: i32) {
unimplemented!()
}
}

impl App {
get!(apps);
insert!(apps, NewApp);
find_by!(apps, find_by_client_id, client_id as String);
}
2 changes: 2 additions & 0 deletions plume-models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ pub fn ap_url(url: String) -> String {
}

pub mod admin;
pub mod api_tokens;
pub mod apps;
pub mod blog_authors;
pub mod blogs;
pub mod comments;
Expand Down
27 changes: 27 additions & 0 deletions plume-models/src/schema.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
table! {
api_tokens (id) {
id -> Int4,
creation_date -> Timestamp,
value -> Text,
scopes -> Text,
app_id -> Int4,
user_id -> Int4,
}
}

table! {
apps (id) {
id -> Int4,
name -> Text,
client_id -> Text,
client_secret -> Text,
redirect_uri -> Nullable<Text>,
website -> Nullable<Text>,
creation_date -> Timestamp,
}
}

table! {
blog_authors (id) {
id -> Int4,
Expand Down Expand Up @@ -172,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));
Expand All @@ -192,6 +217,8 @@ joinable!(tags -> posts (post_id));
joinable!(users -> instances (instance_id));

allow_tables_to_appear_in_same_query!(
api_tokens,
apps,
blog_authors,
blogs,
comments,
Expand Down
Loading