Skip to content

Commit

Permalink
Login via REST API
Browse files Browse the repository at this point in the history
In case a user doesn't want to login via chat message, login via REST API is now possible
  • Loading branch information
exul committed Apr 21, 2017
1 parent da0e861 commit 5ee11fc
Show file tree
Hide file tree
Showing 15 changed files with 469 additions and 70 deletions.
4 changes: 3 additions & 1 deletion assets/translations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ en:
1. Send a message in this room: `login rocketchatusername mysecret`
**Warning**: This will store your password *unecrypted* in the database of your homeserver
1. Login via curl: `curl ${as_url}/login -d '{"rocketchat_server_url": "${rocketchat_url}", "matrix_user_id": "${matrix_user_id}", "username": "rocketchat_user", "password": "mysecret"}'`
1. Login via curl: `curl ${as_url}/rocketchat/login -d '{"rocketchat_server_url": "${rocketchat_url}", "matrix_user_id": "${matrix_user_id}", "username": "rocketchat_user", "password": "mysecret"}'`
no_rocketchat_server_connected: "No Rocket.Chat server is connected yet."
usage_instructions: |
You are logged in.
Expand All @@ -69,7 +69,9 @@ en:
internal_error: "An internal error occurred"
handlers:
welcome: "Your Rocket.Chat <-> Matrix application service is running"
rocketchat_login_successful: "You are logged in."
errors:
admin_room_for_rocketchat_server_not_found: "No admin room found that is connected to the Rocket.Chat server ${rocketchat_url}"
authentication_failed: "Authentication failed!"
internal: "An internal error occurred"
no_rocketchat_server: "No Rocket.Chat server found when querying ${rocketchat_url} (version information is missing from the response)"
Expand Down
1 change: 0 additions & 1 deletion src/matrix-rocketchat/api/rocketchat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ pub trait Endpoint {
}
}

//TODO: Move this into v1, because those structs are depending on the api version as well
/// A Rocket.Chat channel
#[derive(Deserialize, Debug)]
pub struct Channel {
Expand Down
33 changes: 32 additions & 1 deletion src/matrix-rocketchat/db/rocketchat_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ use diesel;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use iron::typemap::Key;
use ruma_identifiers::UserId;

use i18n::*;
use errors::*;
use super::schema::rocketchat_servers;
use super::{Room, User, UserInRoom};
use super::schema::{rocketchat_servers, rooms, users_in_rooms};

/// A Rocket.Chat server.
#[derive(Associations, Debug, Identifiable, Queryable)]
Expand Down Expand Up @@ -75,6 +78,34 @@ impl RocketchatServer {
.chain_err(|| ErrorKind::DBSelectError)?;
Ok(rocketchat_servers)
}

/// Get the admin room for this Rocket.Chat server and a given user.
pub fn admin_room_for_user(&self, connection: &SqliteConnection, matrix_user_id: &UserId) -> Result<Option<Room>> {
let user = match User::find_by_matrix_user_id(connection, matrix_user_id)? {
Some(user) => user,
None => return Ok(None),
};

let rooms =
rooms::table.filter(rooms::is_admin_room.eq(true)
.and(rooms::matrix_room_id.eq_any(UserInRoom::belonging_to(&user)
.select(users_in_rooms::matrix_room_id))))
.load::<Room>(connection)
.chain_err(|| ErrorKind::DBSelectError)?;
Ok(rooms.into_iter().next())
}

/// Same as admin_room_for_user but returns an error if no room is found.
pub fn admin_room_for_user_or_err(&self, connection: &SqliteConnection, matrix_user_id: &UserId) -> Result<Room> {
match self.admin_room_for_user(connection, matrix_user_id)? {
Some(room) => Ok(room),
None => {
Err(user_error!(ErrorKind::AdminRoomForRocketchatServerNotFound(self.rocketchat_url.clone()),
t!(["errors", "admin_room_for_rocketchat_server_not_found"])
.with_vars(vec![("rocketchat_url", self.rocketchat_url.clone())])))
}
}
}
}

impl Key for RocketchatServer {
Expand Down
3 changes: 2 additions & 1 deletion src/matrix-rocketchat/db/user_in_room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use diesel::sqlite::SqliteConnection;
use ruma_identifiers::{RoomId, UserId};

use errors::*;
use super::room::Room;
use super::{Room, User};
use super::schema::users_in_rooms;

/// Join table for users that participate in a room.
#[derive(Associations, Debug, Identifiable, Queryable)]
#[belongs_to(Room, foreign_key = "matrix_room_id")]
#[belongs_to(User, foreign_key = "matrix_user_id")]
#[primary_key(matrix_user_id, matrix_room_id)]
#[table_name="users_in_rooms"]
pub struct UserInRoom {
Expand Down
19 changes: 15 additions & 4 deletions src/matrix-rocketchat/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,21 @@ error_chain!{
display("Reading file from {} failed", path)
}

RoomNotConnected(matrix_room_id: String, command: String) {
RoomNotConnected(matrix_room_id: String) {
description("The room is not connected, but has to be for the command the user submitted")
display("Room {} is not connected to a Rocket.Chat server, cannot execute command {}", matrix_room_id, command)
display("Room {} is not connected to a Rocket.Chat server, cannot execute command", matrix_room_id)
}

RoomAlreadyConnected(matrix_room_id: String) {
description("The Room is already connected to a Rocket.Chat server")
display("Room {} is already connected", matrix_room_id)
}

AdminRoomForRocketchatServerNotFound(rocketchat_url: String) {
description("The user does not have an admin room that is connected to the given Rocket.Chat server")
display("No admin room found that is connected to the Rocket.Chat server {}", rocketchat_url)
}

RocketchatTokenMissing{
description("A token is needed to connect new Rocket.Chat servers")
display("Attempt to connect a Rocket.Chat server without a token")
Expand Down Expand Up @@ -308,6 +313,8 @@ impl Error {
ErrorKind::MissingAccessToken |
ErrorKind::MissingRocketchatToken => Status::Unauthorized,
ErrorKind::InvalidJSON(_) => Status::UnprocessableEntity,
ErrorKind::AdminRoomForRocketchatServerNotFound(_) => Status::NotFound,
ErrorKind::AuthenticationFailed(_) => Status::Unauthorized,
_ => Status::InternalServerError,
}
}
Expand Down Expand Up @@ -363,10 +370,14 @@ impl From<Error> for IronError {

impl<'a> Modifier<Response> for &'a Error {
fn modify(self, response: &mut Response) {
let causes = self.error_chain.iter().skip(1).map(|e| format!("{}", e)).collect();
let error_message = match self.user_message {
Some(ref user_message) => user_message.l(DEFAULT_LANGUAGE),
None => format!("{}", self),
};

let causes = self.error_chain.iter().skip(1).map(|e| format!("{}", e)).collect();
let resp = ErrorResponse {
error: format!("{}", self),
error: format!("{}", &error_message),
causes: causes,
};

Expand Down
7 changes: 3 additions & 4 deletions src/matrix-rocketchat/handlers/error_notifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub struct ErrorNotifier<'a> {
impl<'a> ErrorNotifier<'a> {
/// Send the error message to the user if the error contains a user message. Otherwise just
/// inform the user that an internal error happened.
pub fn send_message_to_user(&self, err: Error, room_id: RoomId, user_id: &UserId) -> Result<()> {
pub fn send_message_to_user(&self, err: &Error, room_id: RoomId, user_id: &UserId) -> Result<()> {
let matrix_bot_id = self.config.matrix_bot_user_id()?;
let language = match User::find_by_matrix_user_id(self.connection, user_id)? {
Some(user) => user.language,
Expand All @@ -34,12 +34,11 @@ impl<'a> ErrorNotifier<'a> {
Some(ref user_message) => user_message,
None => {
let user_msg = t!(["defaults", "internal_error"]).l(&language);
self.matrix_api.send_text_message_event(room_id, matrix_bot_id, user_msg)?;
return Err(err);
return self.matrix_api.send_text_message_event(room_id, matrix_bot_id, user_msg);
}
};

let mut msg = format!("{}", &err);
let mut msg = format!("{}", err);
for err in err.error_chain.iter().skip(1) {
msg = msg + " caused by: " + &format!("{}", err);
}
Expand Down
51 changes: 20 additions & 31 deletions src/matrix-rocketchat/handlers/events/command_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use db::{NewRocketchatServer, NewRoom, NewUserInRoom, NewUserOnRocketchatServer,
UserOnRocketchatServer};
use errors::*;
use handlers::rocketchat::VirtualUserHandler;
use handlers::rocketchat::{Credentials, Login};
use i18n::*;

/// Handles command messages from the admin room
Expand Down Expand Up @@ -58,22 +59,22 @@ impl<'a> CommandHandler<'a> {
} else if message.starts_with("login") {
debug!(self.logger, "Received login command");

let rocketchat_server = self.get_rocketchat_server(room, &message)?;
let rocketchat_server = self.get_rocketchat_server(room)?;
self.login(event, &rocketchat_server, &message)?;
} else if message == "list" {
debug!(self.logger, "Received list command");

let rocketchat_server = self.get_rocketchat_server(room, &message)?;
let rocketchat_server = self.get_rocketchat_server(room)?;
self.list_channels(event, &rocketchat_server)?;
} else if message.starts_with("bridge") {
debug!(self.logger, "Received bridge command");

let rocketchat_server = self.get_rocketchat_server(room, &message)?;
let rocketchat_server = self.get_rocketchat_server(room)?;
self.bridge(event, &rocketchat_server, &message)?;
} else if message.starts_with("unbridge") {
debug!(self.logger, "Received unbridge command");

let rocketchat_server = self.get_rocketchat_server(room, &message)?;
let rocketchat_server = self.get_rocketchat_server(room)?;
self.unbridge(event, &rocketchat_server, &message)?;
} else {
let msg = format!("Skipping event, don't know how to handle command `{}`", message);
Expand Down Expand Up @@ -171,35 +172,23 @@ impl<'a> CommandHandler<'a> {
}

fn login(&self, event: &MessageEvent, rocketchat_server: &RocketchatServer, message: &str) -> Result<()> {
let user = User::find(self.connection, &event.user_id)?;

let mut user_on_rocketchat_server =
UserOnRocketchatServer::find(self.connection, &event.user_id, rocketchat_server.id)?;

let mut command = message.split_whitespace().collect::<Vec<&str>>().into_iter();
let username = command.by_ref().nth(1).unwrap_or_default();
let password = command.by_ref().fold("".to_string(), |acc, x| acc + x);

let mut rocketchat_api = RocketchatApi::new(rocketchat_server.rocketchat_url.clone(), self.logger.clone())?;

let (rocketchat_user_id, rocketchat_auth_token) = rocketchat_api.login(username, &password)?;
user_on_rocketchat_server.set_credentials(self.connection,
Some(rocketchat_user_id.clone()),
Some(rocketchat_auth_token.clone()))?;

rocketchat_api = rocketchat_api.with_credentials(rocketchat_user_id, rocketchat_auth_token);
let username = rocketchat_api.current_username()?;
user_on_rocketchat_server.set_rocketchat_username(self.connection, Some(username.clone()))?;

let bot_matrix_user_id = self.config.matrix_bot_user_id()?;
let room = Room::find(self.connection, &event.room_id)?;
let message = CommandHandler::build_help_message(self.connection, self.config.as_url.clone(), &room, &user)?;
self.matrix_api.send_text_message_event(event.room_id.clone(), bot_matrix_user_id, message)?;

Ok(info!(self.logger,
"Successfully executed login command for user {} on Rocket.Chat server {}",
username,
rocketchat_server.rocketchat_url))
let credentials = Credentials {
matrix_user_id: event.user_id.clone(),
rocketchat_username: username.to_string(),
password: password.to_string(),
rocketchat_url: rocketchat_server.rocketchat_url.clone(),
};
let login = Login {
config: self.config,
connection: self.connection,
logger: self.logger,
matrix_api: self.matrix_api,
};
login.call(&credentials, rocketchat_server)
}

fn list_channels(&self, event: &MessageEvent, rocketchat_server: &RocketchatServer) -> Result<()> {
Expand Down Expand Up @@ -349,11 +338,11 @@ impl<'a> CommandHandler<'a> {
Ok(channel_list)
}

fn get_rocketchat_server(&self, room: &Room, message: &str) -> Result<RocketchatServer> {
fn get_rocketchat_server(&self, room: &Room) -> Result<RocketchatServer> {
match room.rocketchat_server(self.connection)? {
Some(rocketchat_server) => Ok(rocketchat_server),
None => {
Err(user_error!(ErrorKind::RoomNotConnected(room.matrix_room_id.to_string(), message.to_string()),
Err(user_error!(ErrorKind::RoomNotConnected(room.matrix_room_id.to_string()),
t!(["errors", "room_not_connected"])))
}
}
Expand Down
13 changes: 7 additions & 6 deletions src/matrix-rocketchat/handlers/events/event_dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,8 @@ impl<'a> EventDispatcher<'a> {
}
}
Event::RoomMessage(message_event) => {
if let Err(err) = MessageHandler::new(self.config,
self.connection,
self.logger,
self.matrix_api.clone())
.process(&message_event) {
if let Err(err) = MessageHandler::new(self.config, self.connection, self.logger, self.matrix_api.clone())
.process(&message_event) {
return self.handle_error(err, message_event.room_id, &message_event.user_id);
}
}
Expand All @@ -69,6 +66,10 @@ impl<'a> EventDispatcher<'a> {
logger: &self.logger,
matrix_api: &self.matrix_api,
};
error_notifier.send_message_to_user(err, room_id, user_id)
error_notifier.send_message_to_user(&err, room_id, user_id)?;
if err.user_message.is_none() {
return Err(err);
}
Ok(())
}
}
3 changes: 3 additions & 0 deletions src/matrix-rocketchat/handlers/iron/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

/// Process requests from the Rocket.Chat server
pub mod rocketchat;
/// Process login request for Rocket.Chat
pub mod rocketchat_login;
/// Processes requests from the Matrix homeserver
pub mod transactions;
/// Sends a welcome message to the caller
pub mod welcome;

pub use self::rocketchat::Rocketchat;
pub use self::rocketchat_login::RocketchatLogin;
pub use self::transactions::Transactions;
pub use self::welcome::Welcome;
8 changes: 3 additions & 5 deletions src/matrix-rocketchat/handlers/iron/rocketchat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,9 @@ impl Handler for Rocketchat {
let logger = IronLogger::from_request(request)?;
let connection = ConnectionPool::from_request(request)?;

let message =
request.extensions.get::<Message>().expect("Middleware ensures the presence of the Rocket.Chat message");
let rocketchat_server = request.extensions
.get::<RocketchatServer>()
.expect("Middleware ensures the presence of the Rocket.Chat server");
let message = request.extensions.get::<Message>().expect("Middleware ensures the presence of the Rocket.Chat message");
let rocketchat_server =
request.extensions.get::<RocketchatServer>().expect("Middleware ensures the presence of the Rocket.Chat server");

let forwarder = Forwarder {
config: &self.config,
Expand Down
71 changes: 71 additions & 0 deletions src/matrix-rocketchat/handlers/iron/rocketchat_login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::io::Read;

use iron::{Handler, status};
use iron::prelude::*;
use iron::request::Body;
use serde_json;

use i18n::*;
use api::MatrixApi;
use config::Config;
use db::{ConnectionPool, RocketchatServer};
use errors::*;
use handlers::ErrorNotifier;
use handlers::rocketchat::{Credentials, Login};
use log::IronLogger;

/// RocketchatLogin is an endpoint that allows a user to login to Rocket.Chat via REST API.
pub struct RocketchatLogin {
/// Application service configuration
pub config: Config,
/// Matrix REST API
pub matrix_api: Box<MatrixApi>,
}

impl Handler for RocketchatLogin {
fn handle(&self, request: &mut Request) -> IronResult<Response> {
let logger = IronLogger::from_request(request)?;
debug!(logger, "Received login command via REST API");

let connection = ConnectionPool::from_request(request)?;
let credentials = deserialize_credentials(&mut request.body)?;
let login = Login {
config: &self.config,
connection: &connection,
logger: &logger,
matrix_api: &self.matrix_api,
};
let rocketchat_server = match RocketchatServer::find_by_url(&connection, credentials.rocketchat_url.clone())? {
Some(rocketchat_server) => rocketchat_server,
None => {
return Err(user_error!(ErrorKind::AdminRoomForRocketchatServerNotFound(credentials.rocketchat_url.clone()),
t!(["errors", "admin_room_for_rocketchat_server_not_found"])
.with_vars(vec![("rocketchat_url", credentials.rocketchat_url.clone())])))?;
}
};

if let Err(err) = login.call(&credentials, &rocketchat_server) {
if let Some(admin_room) = rocketchat_server.admin_room_for_user(&connection, &credentials.matrix_user_id)? {
let error_notifier = ErrorNotifier {
config: &self.config,
connection: &connection,
logger: &logger,
matrix_api: &self.matrix_api,
};
error_notifier.send_message_to_user(&err, admin_room.matrix_room_id.clone(), &credentials.matrix_user_id)?;
}
return Err(err)?;
}

Ok(Response::with((status::Ok, t!(["handlers", "rocketchat_login_successful"]).l(DEFAULT_LANGUAGE))))
}
}


fn deserialize_credentials(body: &mut Body) -> Result<Credentials> {
let mut payload = String::new();
body.read_to_string(&mut payload).chain_err(|| ErrorKind::InternalServerError)?;
serde_json::from_str(&payload)
.chain_err(|| ErrorKind::InvalidJSON(format!("Could not deserialize login request: `{}`", payload)))
.map_err(Error::from)
}

0 comments on commit 5ee11fc

Please sign in to comment.