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

feat(IAM-170): adding slack to whoami #9

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
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);
16 changes: 12 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,32 @@ 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();
info!("starting dino-park-whoami");
let s = settings::Settings::new()?;
let client = cis_client::CisClient::from_settings(&s.cis)?;
info!("initialized cis_client");
LeoMcA marked this conversation as resolved.
Show resolved Hide resolved
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 +48,13 @@ fn main() -> Result<(), Error> {
&s.whoami,
&secret,
client.clone(),
))
.service(slack_app(
&s.providers.slack,
&s.whoami,
&secret,
Arc::clone(&ttl_cache),
client.clone(),
)),
)
.service(healthz::healthz_app())
Expand Down
10 changes: 10 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@ pub struct GitHub {
pub client_secret: String,
}

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

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

#[derive(Debug, Deserialize, Clone)]
Expand Down
214 changes: 214 additions & 0 deletions src/slack/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
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 futures::future;
use futures::Future;
use futures::IntoFuture;
use log::info;
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 std::sync::RwLock;
use ttl_cache::TtlCache;
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)]
Copy link
Contributor

Choose a reason for hiding this comment

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

we can derive Clone here if we delete the impl below.

pub struct SlackUser {
name: String,
id: String,
}

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

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

#[derive(Deserialize, Debug)]
pub struct SlackTokenResponse {
ok: bool,
access_token: String,
scope: String,
user: SlackUser,
team: SlackTeam,
}

fn redirect(client: web::Data<Arc<BasicClient>>, session: Session) -> impl Responder {
let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random);
info!("Setting csrf_state: {}", csrf_state.secret().clone());
session
.set("csrf_state", csrf_state.secret().clone())
.map(|_| {
HttpResponse::Found()
.header(http::header::LOCATION, authorize_url.to_string())
.finish()
})
}

fn auth<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 code = query.code.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

not needed we can use query.code a few lines later directly without copying here.

let state = CsrfToken::new(query.state.clone());
let slack_token_url = format!(
"{}{}&code={}",
TOKEN_URL,
slack_uri_data.slack_auth_params.to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

.to_string() not needed.

code
);
if let Some(ref must_state) = session.get::<String>("csrf_state").unwrap() {
if must_state != state.secret() {
return Box::new(future::ok(
HttpResponse::Found()
.header(http::header::LOCATION, "/e?identityAdded=error")
.finish(),
));
}
} else {
return Box::new(future::ok(
HttpResponse::Found()
.header(http::header::LOCATION, "/e?identityAdded=error")
.finish(),
LeoMcA marked this conversation as resolved.
Show resolved Hide resolved
));
}
let get = cis_client.clone();
let get_uid = user_id.user_id.clone();
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::<SlackTokenResponse>().map_err(Into::into))
.and_then(move |j| {
get.get_user_by(&get_uid, &GetBy::UserId, None)
.and_then(move |profile: Profile| {
update_slack(
format!(
"{}{}",
slack_uri_data.direct_message_uri.to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

.to_string() is not needed here.

j.user.id
),
j.user.name,
profile,
get.get_secret_store(),
)
.into_future()
.map_err(Into::into)
})
.map_err(Into::into)
})
.and_then(move |profile: Profile| {
cis_client
.update_user(&user_id.user_id, profile)
.map_err(Into::into)
})
.and_then(|_| {
HttpResponse::Found()
.header(http::header::LOCATION, "/e?identityAdded=slack")
.finish()
}),
)
}

pub fn slack_app<T: AsyncCisClientTrait + 'static>(
slack: &Slack,
whoami: &WhoAmI,
secret: &[u8],
ttl_cache: Arc<RwLock<TtlCache<String, String>>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

That's not used in here. We only use it in the GitHub app to cache usernames.

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 slack_auth_params = format!(
"?client_id={}&client_secret={}&redirect_uri={}",
&slack.client_id, &slack.client_secret, &slack.redirect_uri
);
let auth_url = AuthUrl::new(Url::parse(AUTH_URL).expect("Invalid authorization endpoint URL"));
let token_url = TokenUrl::new(
Url::parse(&format!("{}{}", TOKEN_URL, slack_auth_params))
.expect("Invalid token endpoint URL"),
);

let client = Arc::new(
BasicClient::new(
slack_client_id,
Some(slack_client_secret),
auth_url,
Some(token_url),
)
.add_scope(Scope::new(slack.scope.to_string()))
Copy link
Contributor

Choose a reason for hiding this comment

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

.clone() insteadl of .to_string() to me more explicit.

.set_redirect_url(RedirectUrl::new(
Url::parse(&slack.redirect_uri).expect("Invalid redirect URL"),
)),
);
let slack_uri_data: SlackUriData = SlackUriData {
slack_auth_params,
direct_message_uri: slack.direct_message_uri.to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

that should be a .clone() instead of .to_string() to make it more clear what's happening.

};

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(client)
.data(cis_client)
.data(ttl_cache)
.data(slack_uri_data)
.service(web::resource("/add").route(web::get().to(redirect)))
.service(web::resource("/auth").route(web::get().to_async(auth::<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;
15 changes: 15 additions & 0 deletions src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ 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 uris_kv_pairs = vec![(
andrew-sunada marked this conversation as resolved.
Show resolved Hide resolved
"EA#SLACK#n".to_string(),
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