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

Slim down slack #10

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
230 changes: 140 additions & 90 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ oauth2 = "2.0.0-beta.3"
url = "1.7"
base64 = "0.10.1"
rand = "0.7"
actix-web = { version = "1.0", features = ["ssl"] }
actix-web = { version = "1.0", features = ["rust-tls"] }
actix-cors = "0.1"
actix-session = "0.2"
failure = "0.1.5"
Expand All @@ -27,4 +27,4 @@ futures = "0.1"
chrono = "0.4.6"
env_logger = "0.6.1"
log = "0.4.6"
ttl_cache = "0.5.1"
ttl_cache = "0.5.1"
24 changes: 13 additions & 11 deletions proxy/proxy.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
const fs = require('fs');
const fs = require('fs');
const httpProxy = require('http-proxy');

//
// Create the HTTPS proxy server listening on port 8000
//
httpProxy.createServer({
target: {
host: '127.0.0.1',
port: 8084,
},
ssl: {
key: fs.readFileSync(process.env["DP_HTTPS_KEY"], 'utf8'),
cert: fs.readFileSync(process.env["DP_HTTPS_CERT"], 'utf8')
}
}).listen(443);
httpProxy
.createServer({
target: {
host: '127.0.0.1',
port: 8084,
},
ssl: {
key: fs.readFileSync(process.env['DP_HTTPS_KEY'], 'utf8'),
cert: fs.readFileSync(process.env['DP_HTTPS_CERT'], 'utf8'),
},
})
.listen(443);
14 changes: 11 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ mod bugzilla;
mod github;
mod healthz;
mod settings;
mod slack;
mod update;
mod userid;

use crate::bugzilla::app::bugzilla_app;
use crate::github::app::github_app;
use crate::slack::app::slack_app;
use actix_web::middleware::Logger;
use actix_web::web;
use actix_web::App;
use actix_web::HttpServer;
use failure::Error;
use log::info;
use std::sync::Arc;
use std::sync::RwLock;
use ttl_cache::TtlCache;

use actix_web::HttpServer;
use failure::Error;

fn main() -> Result<(), Error> {
std::env::set_var("RUST_LOG", "info");
env_logger::init();
Expand All @@ -30,6 +31,7 @@ fn main() -> Result<(), Error> {
info!("initialized cis_client");
let secret = base64::decode(&s.whoami.secret)?;
let ttl_cache = Arc::new(RwLock::new(TtlCache::<String, String>::new(2000)));

HttpServer::new(move || {
App::new()
.wrap(Logger::default().exclude("/healthz"))
Expand All @@ -47,6 +49,12 @@ fn main() -> Result<(), Error> {
&s.whoami,
&secret,
client.clone(),
))
.service(slack_app(
&s.providers.slack,
&s.whoami,
&secret,
client.clone(),
)),
)
.service(healthz::healthz_app())
Expand Down
11 changes: 11 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ pub struct GitHub {
pub client_secret: String,
}

#[derive(Debug, Deserialize, Clone)]
pub struct Slack {
pub client_id: String,
pub client_secret: String,
pub identity_scope: String,
pub identity_redirect_uri: String,
pub direct_message_uri: String,
pub team_id: String,
}

#[derive(Debug, Deserialize, Clone)]
pub struct Providers {
pub github: GitHub,
pub bugzilla: BugZilla,
pub slack: Slack,
}

#[derive(Debug, Deserialize, Clone)]
Expand Down
218 changes: 218 additions & 0 deletions src/slack/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
use crate::settings::Slack;
use crate::settings::WhoAmI;
use crate::update::update_slack;
use crate::userid::UserId;
use actix_cors::Cors;
use actix_session::CookieSession;
use actix_session::Session;
use actix_web::client::Client;
use actix_web::cookie::SameSite;
use actix_web::dev::HttpServiceFactory;
use actix_web::http;
use actix_web::web;
use actix_web::Error;
use actix_web::HttpResponse;
use actix_web::Responder;
use cis_client::getby::GetBy;
use cis_client::AsyncCisClientTrait;
use cis_profile::schema::Profile;
use failure::format_err;
use futures::future;
use futures::Future;
use futures::IntoFuture;
use log::error;
use oauth2::basic::BasicClient;
use oauth2::prelude::*;
use oauth2::AuthUrl;
use oauth2::ClientId;
use oauth2::ClientSecret;
use oauth2::CsrfToken;
use oauth2::RedirectUrl;
use oauth2::Scope;
use oauth2::TokenUrl;
use std::sync::Arc;
use url::Url;

const AUTH_URL: &str = "https://slack.com/oauth/authorize";
const TOKEN_URL: &str = "https://slack.com/api/oauth.access";

#[derive(Deserialize)]
pub struct Auth {
code: String,
state: String,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct SlackUser {
name: String,
id: String,
}

#[derive(Deserialize, Serialize, Debug)]
pub struct SlackUriData {
identity_slack_auth_params: String,
direct_message_uri: String,
team_id: String,
}

#[derive(Deserialize, Debug, Clone)]
pub struct SlackUserTokenResponse {
ok: bool,
access_token: String,
scope: String,
user_id: String,
team_id: String,
user: SlackUser,
}

fn send_response(url: &str) -> HttpResponse {
HttpResponse::Found()
.header(http::header::LOCATION, url)
.finish()
}

fn send_error_response() -> HttpResponse {
send_response("/e?identityAdded=error")
}

/**
* First redirect that handles getting authorization for identity scopes
*/
fn redirect_identity(client: web::Data<Arc<BasicClient>>, session: Session) -> impl Responder {
let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random);
session
.set("identity_csrf_state", csrf_state.secret().clone())
.map(|_| send_response(&authorize_url.to_string()))
}

fn auth_identity<T: AsyncCisClientTrait + 'static>(
cis_client: web::Data<T>,
user_id: UserId,
query: web::Query<Auth>,
slack_uri_data: web::Data<SlackUriData>,
session: Session,
) -> Box<dyn Future<Item = HttpResponse, Error = Error>> {
let state = CsrfToken::new(query.state.clone());
let slack_token_url = format!(
"{}{}&code={}&team={}",
TOKEN_URL,
slack_uri_data.identity_slack_auth_params,
query.code.clone(),
slack_uri_data.team_id
);
// Check state token from im_crsf_state
if let Some(ref must_state) = session.get::<String>("identity_csrf_state").unwrap() {
if must_state != state.secret() {
error!("Error: Identity csrf state mismatch");
return Box::new(future::ok(send_error_response()));
}
} else {
error!("Error: Missing identity csrf state");
return Box::new(future::ok(send_error_response()));
}

let get = cis_client.clone();
let get_uid = user_id.user_id.clone();
let enforced_team_id = slack_uri_data.team_id.clone();
// Begin slack requests by grabbing the user_id, and access_token
Box::new(
Client::default()
.get(slack_token_url)
.header(http::header::USER_AGENT, "whoami")
.send()
.map_err(Into::into)
.and_then(move |mut res| res.json::<SlackUserTokenResponse>().map_err(Into::into))
.and_then(move |sur| {
if sur.team_id == enforced_team_id {
Ok(sur)
} else {
Err(format_err!("slack team_id missmatch").into())
}
})
.and_then(move |sur| {
// Now that we have the access_token, user data, and channel id, go put it in the profile
get.get_user_by(&get_uid, &GetBy::UserId, None)
.and_then(move |profile: Profile| {
update_slack(
format!(
"{}?channel={}&team={}",
slack_uri_data.direct_message_uri, sur.user_id, sur.team_id
),
sur.user.name.clone(),
Copy link

Choose a reason for hiding this comment

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

This returns "Leo McArdle" rather than "leo", is there no way of returning the username without getting into scope hell again?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not hell, but complicated. The display name would require another scope and another call to an endpoint.

Copy link

Choose a reason for hiding this comment

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

Okay, I don't think it's necessary for the moment then.

profile,
get.get_secret_store(),
)
.into_future()
.map_err(Into::into)
})
.map_err(Into::into)
})
.and_then(move |profile: Profile| {
// Now finally save the updated user profile
cis_client
.update_user(&user_id.user_id, profile)
.map_err(Into::into)
})
.and_then(|_| send_response("/e?identityAdded=slack")),
)
}

pub fn slack_app<T: AsyncCisClientTrait + 'static>(
slack: &Slack,
whoami: &WhoAmI,
secret: &[u8],
cis_client: T,
) -> impl HttpServiceFactory {
let slack_client_id = ClientId::new(slack.client_id.clone());
let slack_client_secret = ClientSecret::new(slack.client_secret.clone());
let identity_slack_auth_params = format!(
"?client_id={}&client_secret={}&redirect_uri={}",
&slack.client_id, &slack.client_secret, &slack.identity_redirect_uri
);
let auth_url = AuthUrl::new(Url::parse(AUTH_URL).expect("Invalid authorization endpoint URL"));
let identity_token_url = TokenUrl::new(
Url::parse(&format!("{}{}", TOKEN_URL, identity_slack_auth_params))
.expect("Invalid token endpoint URL"),
);
let identity_client = Arc::new(
BasicClient::new(
slack_client_id.clone(),
Some(slack_client_secret.clone()),
auth_url.clone(),
Some(identity_token_url),
)
.add_scope(Scope::new(slack.identity_scope.clone()))
.set_redirect_url(RedirectUrl::new(
Url::parse(&slack.identity_redirect_uri).expect("Invalid redirect URL"),
)),
);
let slack_uri_data: SlackUriData = SlackUriData {
identity_slack_auth_params,
direct_message_uri: slack.direct_message_uri.clone(),
team_id: slack.team_id.clone(),
};

web::scope("/slack/")
.wrap(
Cors::new()
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600),
)
.wrap(
CookieSession::private(secret)
.name("dpw_s")
.path("/whoami/slack")
.domain(whoami.domain.clone())
.same_site(SameSite::Lax)
.http_only(true)
.secure(false)
.max_age(300),
)
.data(identity_client)
.data(cis_client)
.data(slack_uri_data)
.service(web::resource("/add").route(web::get().to(redirect_identity)))
.service(web::resource("/auth/identity").route(web::get().to_async(auth_identity::<T>)))
}
1 change: 1 addition & 0 deletions src/slack/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod app;
22 changes: 22 additions & 0 deletions src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ pub fn update_bugzilla(
Ok(profile)
}

pub fn update_slack(
slack_uri: String,
slack_username: String,
mut profile: Profile,
store: &SecretStore,
) -> Result<Profile, Error> {
let now = &Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let mut key_val: String = String::from("EA#SLACK#n");
if let Some(KeyValue(ref mut values)) = &mut profile.uris.values {
for (k, _) in values.iter_mut() {
if k.contains("#SLACK#") {
let last_char = k.chars().last().unwrap();
key_val = String::from("EA#SLACK#");
key_val.push(last_char);
}
}
}
let uris_kv_pairs = vec![(key_val, format!("{}#{}", slack_uri, slack_username))];
update_and_sign_values_field(&mut profile.uris, uris_kv_pairs, store, &now)?;
Ok(profile)
}

fn update_and_sign_values_field(
field: &mut StandardAttributeValues,
kv_pairs: Vec<(String, String)>,
Expand Down