(barely) working /manage endpoint

This commit is contained in:
Vincent Breitmoser 2019-04-05 17:07:40 +02:00
parent 440bf662fd
commit 2b1722c540
No known key found for this signature in database
GPG Key ID: 7BD18320DEADFA11
15 changed files with 300 additions and 11 deletions

View File

@ -16,7 +16,7 @@ use {Error, Result};
/// - Lower-case the whole thing using the empty locale
///
/// See https://autocrypt.org/level1.html#e-mail-address-canonicalization
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Email(String);
impl Email {

27
dist/templates/email/manage-html.hbs vendored Normal file
View File

@ -0,0 +1,27 @@
<!doctype html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>Please confirm deletion of your key</title>
</head>
<body>
<p>
Someone, presumably you, requested that your key will no longer
be available when searching for you email addresses on
<a href="{{base_uri}}">{{domain}}</a>.
</p>
<p>
If it was not you, please ignore this email.
</p>
<p>
To de-list your key on <a href="{{base_uri}}">{{domain}}</a>,
follow this link:
</p>
<p>
<a href="{{base_uri}}{{uri}}">{{base_uri}}{{uri}}</a>
</p>
<p>
If you do not want to de-list your key, please ignore this
email.
</body>
</html>

10
dist/templates/email/manage-txt.hbs vendored Normal file
View File

@ -0,0 +1,10 @@
Someone, presumably you, requested that your key will no longer be
available when searching for you email addresses on {{domain}}.
If it was not you, please ignore this email.
To de-list your key on {{domain}}, follow this link:
{{base_uri}}{{uri}}
If you do not want to de-list your key, please ignore this email.

View File

@ -1,7 +1,7 @@
{{#> layout }}
{{> search-form}}
<div class="row">
<p>You can also <a href="/publish">upload</a> or <a href="/delete">delete</a> your key.</p>
<p>You can also <a href="/publish">upload</a> or <a href="/vks/manage">delete</a> your key.</p>
</div>
<div class="row">

21
dist/templates/manage/manage.html.hbs vendored Normal file
View File

@ -0,0 +1,21 @@
{{#> layout }}
<div class="row">
<form action="/vks/manage" method="POST">
<div class="manage">
<input type="text" name="search_term" class="manageEmail" autofocus
placeholder="Your Email / Key ID / Fingerprint">
<button type="submit" class="manageButton button">
Manage
</button>
</div>
</form>
</div>
<div class="row">
<p>Please enter one of your email addesses.</p>
</div>
<div class="row" style="text-align: left;">
<strong><a href="/">&laquo; Back</a></strong>
</div>
{{/layout}}

View File

@ -0,0 +1,6 @@
{{#> layout }}
<div class="row">
<p>This management link is invalid or expired.
<p>You can <a href="/vks/manage">click here</a> to request a new one.
</div>
{{/layout}}

View File

@ -0,0 +1,28 @@
{{#> layout }}
<div class="row">
<div>
{{#if uid_unpublished}}
<div class="flash">Unpublished address: <tt>{{uid_unpublished}}</tt></div>
{{/if}}
<div style="width: 80%; margin-left: auto; margin-right: auto; text-align: left;">
{{#each uid_status}}
<div>
<div style="float: right;">
<form action="/vks/manage/unpublish" method="post">
<input type="hidden" name="token" value="{{../token}}" />
<input type="hidden" name="address" value="{{address}}" />
<i class="fa fa-eye-slash"></i>&nbsp;<input type="submit" class="link" value="Unpublish">
</form>
</div>
<p>
{{#if published}}
<i class="fa fa-envelope"></i> <tt>{{address}}</tt>
{{else}}
<tt>{{address}}</tt>
{{/if}}
</div>
{{/each}}
</div>
</div>
</div>
{{/layout}}

View File

@ -0,0 +1,8 @@
{{#> layout }}
<p>
We have sent an email with further instructions to <tt>{{address}}</tt>.
</p>
<div style="text-align: left;">
<strong><a href="/">&laquo; Back</a></strong>
</div>
{{/layout}}

View File

@ -19,7 +19,7 @@
</div>
<div class="row">
<p>You can delete an uploaded key <a href="/delete">here</a>.</p>
<p>You can delete an uploaded key <a href="/vks/manage">here</a>.</p>
</div>
<script>

View File

@ -23,7 +23,7 @@ mod context {
}
#[derive(Serialize, Clone)]
pub struct Deletion {
pub struct Manage {
pub uri: String,
pub base_uri: String,
pub domain: String,
@ -92,6 +92,22 @@ impl Service {
)
}
pub fn send_manage_token(&self, recipients: &[Email], uri: &str)
-> Result<()> {
let ctx = context::Manage {
uri: uri.to_string(),
base_uri: self.base_uri.clone(),
domain: self.domain.clone(),
};
self.send(
recipients,
&format!("{}: Manage your key", &self.domain),
"manage",
ctx,
)
}
fn send<T>(&self, to: &[Email], subject: &str, template: &str, ctx: T)
-> Result<()>
where T: Serialize + Clone,

View File

@ -2,6 +2,7 @@ use sealed_state::SealedState;
use database::types::{Fingerprint};
use serde_json;
use Result;
const REVISION: u8 = 1;
@ -36,15 +37,17 @@ impl Service {
base64::encode_config(&token_sealed, base64::URL_SAFE_NO_PAD)
}
pub fn check(&self, token_encoded: &str) -> Result<Fingerprint, String> {
let token_sealed = base64::decode_config(&token_encoded, base64::URL_SAFE_NO_PAD).map_err(|_| "invalid b64")?;
let token_str = self.sealed_state.unseal(token_sealed)?;
pub fn check(&self, token_encoded: &str) -> Result<Fingerprint> {
let token_sealed = base64::decode_config(&token_encoded, base64::URL_SAFE_NO_PAD)
.map_err(|_| failure::err_msg("invalid b64"))?;
let token_str = self.sealed_state.unseal(token_sealed)
.map_err(|_| failure::err_msg("failed to validate"))?;
let token: Token = serde_json::from_str(&token_str)
.map_err(|_| "failed to deserialize")?;
.map_err(|_| failure::err_msg("failed to deserialize"))?;
let elapsed = current_time() - token.creation;
if elapsed > self.validity {
Err("Token has expired!")?;
Err(failure::err_msg("Token has expired!"))?;
}
Ok(token.fpr)

160
src/web/manage.rs Normal file
View File

@ -0,0 +1,160 @@
use rocket;
use rocket::State;
use rocket::request::Form;
use failure::Fallible as Result;
use web::{MyResponse,templates::General};
use database::{Database, Polymorphic};
use database::types::Email;
use mail;
use tokens;
mod templates {
#[derive(Serialize)]
pub struct ManageKey {
// pub uid_unpublished: Option<String>,
pub uid_status: Vec<ManageKeyUidStatus>,
pub token: String,
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
pub struct ManageLinkSent {
pub address: String,
}
#[derive(Serialize)]
pub struct ManageKeyUidStatus {
pub address: String,
pub published: bool,
}
}
pub mod forms {
#[derive(FromForm)]
pub struct ManageRequest {
pub search_term: String,
}
#[derive(FromForm)]
pub struct ManageDelete {
pub token: String,
pub address: String,
}
}
#[get("/vks/manage")]
pub fn vks_manage() -> Result<MyResponse> {
Ok(MyResponse::ok("manage/manage", General::default()))
}
#[get("/vks/manage/<token>")]
pub fn vks_manage_key(
db: State<Polymorphic>,
token: String,
token_service: rocket::State<tokens::Service>,
) -> MyResponse {
if let Ok(fingerprint) = token_service.check(&token) {
match db.lookup(&database::Query::ByFingerprint(fingerprint)) {
Ok(Some(tpk)) => {
let mut emails: Vec<Email> = tpk.userids()
.map(|u| u.userid().to_string().parse::<Email>())
.flatten()
.collect();
emails.sort_unstable();
emails.dedup();
let uid_status = emails.into_iter().map(|email|
templates::ManageKeyUidStatus {
address: email.to_string(),
published: true,
}
).collect();
let context = templates::ManageKey {
uid_status,
token,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("manage/manage_key", context)
},
Ok(None) => MyResponse::not_found(
Some("manage/manage"),
Some("This link is invalid or expired".to_owned())),
Err(e) => MyResponse::ise(e),
}
} else {
MyResponse::ok("manage/manage_expired", General::default())
}
}
#[post("/vks/manage", data="<request>")]
pub fn vks_manage_post(
db: State<Polymorphic>,
request: Form<forms::ManageRequest>,
token_service: rocket::State<tokens::Service>,
mail_service: Option<rocket::State<mail::Service>>,
) -> MyResponse {
use std::convert::TryInto;
let email = match request.search_term.parse::<Email>() {
Ok(email) => email,
Err(_) => return MyResponse::not_found(
Some("manage/manage"),
Some(format!("Malformed email address: {:?}", request.search_term)))
};
let tpk = match db.lookup(&database::Query::ByEmail(email.clone())) {
Ok(Some(tpk)) => tpk,
Ok(None) => return MyResponse::not_found(
Some("manage/manage"),
Some(format!("No key for address {:?}", request.search_term))),
Err(e) => return MyResponse::ise(e),
};
let fpr = tpk.fingerprint().try_into().unwrap();
let token = token_service.create(&fpr);
let token_uri = uri!(vks_manage_key: token).to_string();
if let Some(mail_service) = mail_service {
for binding in tpk.userids() {
let email_candidate = binding.userid().to_string().parse::<Email>();
if let Ok(email_candidate) = email_candidate {
if &email_candidate != &email {
continue;
}
if let Err(e) = mail_service.send_manage_token(
&[email_candidate], &token_uri) {
return MyResponse::ise(e);
}
}
}
}
let ctx = templates::ManageLinkSent {
address: email.to_string(),
};
MyResponse::ok("manage/manage_link_sent", ctx)
}
#[post("/vks/manage/unpublish", data="<request>")]
pub fn vks_manage_unpublish(
db: rocket::State<Polymorphic>,
token_service: rocket::State<tokens::Service>,
request: Form<forms::ManageDelete>,
) -> MyResponse {
match vks_manage_unpublish_or_fail(db, token_service, request) {
Ok(response) => response,
Err(e) => MyResponse::ise(e),
}
}
pub fn vks_manage_unpublish_or_fail(
db: rocket::State<Polymorphic>,
token_service: rocket::State<tokens::Service>,
request: Form<forms::ManageDelete>,
) -> Result<MyResponse> {
let fpr = token_service.check(&request.token)?;
let email = request.address.parse::<Email>()?;
db.delete_userids_matching(&fpr, &email)?;
Ok(vks_manage_key(db, request.token.to_owned(), token_service))
}

View File

@ -19,6 +19,7 @@ use Result;
use std::str::FromStr;
mod hkp;
mod manage;
use rocket::http::hyper::header::ContentDisposition;
@ -354,6 +355,11 @@ fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
// HKP
hkp::pks_lookup,
hkp::pks_add,
// EManage
manage::vks_manage,
manage::vks_manage_key,
manage::vks_manage_post,
manage::vks_manage_unpublish,
];
use database::{Filesystem, Polymorphic};
@ -375,12 +381,16 @@ fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
// Mail service
let template_dir: PathBuf = rocket.config().get_str("template_dir")?.into();
let from = rocket.config().get_str("from")?.to_string();
let verify_html = template_dir.join("publish-email-html.hbs");
let verify_txt = template_dir.join("publish-email-txt.hbs");
let verify_html = template_dir.join("email/publish-html.hbs");
let verify_txt = template_dir.join("email/publish-txt.hbs");
let manage_html = template_dir.join("email/manage-html.hbs");
let manage_txt = template_dir.join("email/manage-txt.hbs");
let mut handlebars = Handlebars::new();
handlebars.register_template_file("verify-html", verify_html)?;
handlebars.register_template_file("verify-txt", verify_txt)?;
handlebars.register_template_file("manage-html", manage_html)?;
handlebars.register_template_file("manage-txt", manage_txt)?;
let filemail_into = rocket.config().get_str("filemail_into")
.ok().map(|p| PathBuf::from(p));