aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2022-05-31 15:30:32 +0200
committerAlex Auvolat <alex@adnab.me>2022-05-31 15:30:32 +0200
commitd53cf1d220ef08c0b9368cfe91bee7660b7f5a3b (patch)
tree59e9b4b311553f5feb5a533ddc994074ccde6f6e /src
parent8192d062baf7f783af9519b9419647dc907a3c8c (diff)
downloadaerogramme-d53cf1d220ef08c0b9368cfe91bee7660b7f5a3b.tar.gz
aerogramme-d53cf1d220ef08c0b9368cfe91bee7660b7f5a3b.zip
Implement public_login
Diffstat (limited to 'src')
-rw-r--r--src/config.rs2
-rw-r--r--src/login/ldap_provider.rs120
-rw-r--r--src/login/mod.rs12
-rw-r--r--src/login/static_provider.rs134
4 files changed, 194 insertions, 74 deletions
diff --git a/src/config.rs b/src/config.rs
index b77288b..a1de5ba 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -23,6 +23,8 @@ pub struct LoginStaticConfig {
#[derive(Deserialize, Debug, Clone)]
pub struct LoginStaticUser {
+ #[serde(default)]
+ pub email_addresses: Vec<String>,
pub password: String,
pub aws_access_key_id: String,
diff --git a/src/login/ldap_provider.rs b/src/login/ldap_provider.rs
index c9d23a0..9310e55 100644
--- a/src/login/ldap_provider.rs
+++ b/src/login/ldap_provider.rs
@@ -84,11 +84,30 @@ impl LdapLoginProvider {
bucket_source,
})
}
+
+ fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result<StorageCredentials> {
+ let aws_access_key_id = get_attr(user, &self.aws_access_key_id_attr)?;
+ let aws_secret_access_key = get_attr(user, &self.aws_secret_access_key_attr)?;
+ let bucket = match &self.bucket_source {
+ BucketSource::Constant(b) => b.clone(),
+ BucketSource::Attr(a) => get_attr(user, a)?,
+ };
+
+ Ok(StorageCredentials {
+ k2v_region: self.k2v_region.clone(),
+ s3_region: self.s3_region.clone(),
+ aws_access_key_id,
+ aws_secret_access_key,
+ bucket,
+ })
+ }
}
#[async_trait]
impl LoginProvider for LdapLoginProvider {
async fn login(&self, username: &str, password: &str) -> Result<Credentials> {
+ check_identifier(username)?;
+
let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?;
ldap3::drive!(conn);
@@ -97,13 +116,6 @@ impl LoginProvider for LdapLoginProvider {
ldap.simple_bind(dn, pw).await?.success()?;
}
- let username_is_ok = username
- .chars()
- .all(|c| c.is_alphanumeric() || "-+_.@".contains(c));
- if !username_is_ok {
- bail!("Invalid username, must contain only a-z A-Z 0-9 - + _ . @");
- }
-
let (matches, _res) = ldap
.search(
&self.search_base,
@@ -137,32 +149,9 @@ impl LoginProvider for LdapLoginProvider {
.context("Invalid password")?;
debug!("Ldap login with user name {} successfull", username);
- let get_attr = |attr: &str| -> Result<String> {
- Ok(user
- .attrs
- .get(attr)
- .ok_or(anyhow!("Missing attr: {}", attr))?
- .iter()
- .next()
- .ok_or(anyhow!("No value for attr: {}", attr))?
- .clone())
- };
- let aws_access_key_id = get_attr(&self.aws_access_key_id_attr)?;
- let aws_secret_access_key = get_attr(&self.aws_secret_access_key_attr)?;
- let bucket = match &self.bucket_source {
- BucketSource::Constant(b) => b.clone(),
- BucketSource::Attr(a) => get_attr(a)?,
- };
+ let storage = self.storage_creds_from_ldap_user(&user)?;
- let storage = StorageCredentials {
- k2v_region: self.k2v_region.clone(),
- s3_region: self.s3_region.clone(),
- aws_access_key_id,
- aws_secret_access_key,
- bucket,
- };
-
- let user_secret = get_attr(&self.user_secret_attr)?;
+ let user_secret = get_attr(&user, &self.user_secret_attr)?;
let alternate_user_secrets = match &self.alternate_user_secrets_attr {
None => vec![],
Some(a) => user.attrs.get(a).cloned().unwrap_or_default(),
@@ -178,4 +167,71 @@ impl LoginProvider for LdapLoginProvider {
Ok(Credentials { storage, keys })
}
+
+ async fn public_login(&self, email: &str) -> Result<PublicCredentials> {
+ check_identifier(email)?;
+
+ let (dn, pw) = match self.bind_dn_and_pw.as_ref() {
+ Some(x) => x,
+ None => bail!("Missing bind_dn and bind_password in LDAP login provider config"),
+ };
+
+ let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?;
+ ldap3::drive!(conn);
+ ldap.simple_bind(dn, pw).await?.success()?;
+
+ let (matches, _res) = ldap
+ .search(
+ &self.search_base,
+ Scope::Subtree,
+ &format!(
+ "(&(objectClass=inetOrgPerson)({}={}))",
+ self.mail_attr, email
+ ),
+ &self.attrs_to_retrieve,
+ )
+ .await?
+ .success()?;
+
+ if matches.is_empty() {
+ bail!("No such user account");
+ }
+ if matches.len() > 1 {
+ bail!("Multiple matching user accounts");
+ }
+ let user = SearchEntry::construct(matches.into_iter().next().unwrap());
+ debug!("Found matching LDAP user for email {}: {}", email, user.dn);
+
+ let storage = self.storage_creds_from_ldap_user(&user)?;
+ drop(ldap);
+
+ let k2v_client = storage.k2v_client()?;
+ let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?;
+
+ Ok(PublicCredentials {
+ storage,
+ public_key,
+ })
+ }
+}
+
+fn get_attr(user: &SearchEntry, attr: &str) -> Result<String> {
+ Ok(user
+ .attrs
+ .get(attr)
+ .ok_or(anyhow!("Missing attr: {}", attr))?
+ .iter()
+ .next()
+ .ok_or(anyhow!("No value for attr: {}", attr))?
+ .clone())
+}
+
+fn check_identifier(id: &str) -> Result<()> {
+ let is_ok = id
+ .chars()
+ .all(|c| c.is_alphanumeric() || "-+_.@".contains(c));
+ if !is_ok {
+ bail!("Invalid username/email address, must contain only a-z A-Z 0-9 - + _ . @");
+ }
+ Ok(())
}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 2640a58..c0e9032 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -24,6 +24,9 @@ pub trait LoginProvider {
/// The login method takes an account's password as an input to decypher
/// decryption keys and obtain full access to the user's account.
async fn login(&self, username: &str, password: &str) -> Result<Credentials>;
+ /// The public_login method takes an account's email address and returns
+ /// public credentials for adding mails to the user's inbox.
+ async fn public_login(&self, email: &str) -> Result<PublicCredentials>;
}
/// The struct Credentials represent all of the necessary information to interact
@@ -36,6 +39,13 @@ pub struct Credentials {
pub keys: CryptoKeys,
}
+#[derive(Clone, Debug)]
+pub struct PublicCredentials {
+ /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V)
+ pub storage: StorageCredentials,
+ pub public_key: PublicKey,
+}
+
/// The struct StorageCredentials contains access key to an S3 and K2V bucket
#[derive(Clone, Debug)]
pub struct StorageCredentials {
@@ -396,7 +406,7 @@ impl CryptoKeys {
Ok((salt_ct, public_ct))
}
- async fn load_salt_and_public(k2v: &K2vClient) -> Result<([u8; 32], PublicKey)> {
+ pub async fn load_salt_and_public(k2v: &K2vClient) -> Result<([u8; 32], PublicKey)> {
let mut params = k2v
.read_batch(&[
k2v_read_single_key("keys", "salt", false),
diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs
index cc6ffb6..aa5e499 100644
--- a/src/login/static_provider.rs
+++ b/src/login/static_provider.rs
@@ -1,4 +1,5 @@
use std::collections::HashMap;
+use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
@@ -10,16 +11,34 @@ use crate::login::*;
pub struct StaticLoginProvider {
default_bucket: Option<String>,
- users: HashMap<String, LoginStaticUser>,
+ users: HashMap<String, Arc<LoginStaticUser>>,
+ users_by_email: HashMap<String, Arc<LoginStaticUser>>,
+
k2v_region: Region,
s3_region: Region,
}
impl StaticLoginProvider {
pub fn new(config: LoginStaticConfig, k2v_region: Region, s3_region: Region) -> Result<Self> {
+ let users = config
+ .users
+ .into_iter()
+ .map(|(k, v)| (k, Arc::new(v)))
+ .collect::<HashMap<_, _>>();
+ let mut users_by_email = HashMap::new();
+ for (_, u) in users.iter() {
+ for m in u.email_addresses.iter() {
+ if users_by_email.contains_key(m) {
+ bail!("Several users have same email address: {}", m);
+ }
+ users_by_email.insert(m.clone(), u.clone());
+ }
+ }
+
Ok(Self {
default_bucket: config.default_bucket,
- users: config.users,
+ users,
+ users_by_email,
k2v_region,
s3_region,
})
@@ -29,49 +48,82 @@ impl StaticLoginProvider {
#[async_trait]
impl LoginProvider for StaticLoginProvider {
async fn login(&self, username: &str, password: &str) -> Result<Credentials> {
- match self.users.get(username) {
+ let user = match self.users.get(username) {
None => bail!("User {} does not exist", username),
- Some(u) => {
- if !verify_password(password, &u.password)? {
- bail!("Wrong password");
- }
- let bucket = u
- .bucket
- .clone()
- .or_else(|| self.default_bucket.clone())
- .ok_or(anyhow!(
- "No bucket configured and no default bucket specieid"
- ))?;
-
- let storage = StorageCredentials {
- k2v_region: self.k2v_region.clone(),
- s3_region: self.s3_region.clone(),
- aws_access_key_id: u.aws_access_key_id.clone(),
- aws_secret_access_key: u.aws_secret_access_key.clone(),
- bucket,
- };
+ Some(u) => u,
+ };
- let keys = match (&u.master_key, &u.secret_key) {
- (Some(m), Some(s)) => {
- let master_key = Key::from_slice(&base64::decode(m)?)
- .ok_or(anyhow!("Invalid master key"))?;
- let secret_key = SecretKey::from_slice(&base64::decode(s)?)
- .ok_or(anyhow!("Invalid secret key"))?;
- CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await?
- }
- (None, None) => {
- let user_secrets = UserSecrets {
- user_secret: u.user_secret.clone(),
- alternate_user_secrets: u.alternate_user_secrets.clone(),
- };
- CryptoKeys::open(&storage, &user_secrets, password).await?
- }
- _ => bail!("Either both master and secret key or none of them must be specified for user"),
- };
+ if !verify_password(password, &user.password)? {
+ bail!("Wrong password");
+ }
+ let bucket = user
+ .bucket
+ .clone()
+ .or_else(|| self.default_bucket.clone())
+ .ok_or(anyhow!(
+ "No bucket configured and no default bucket specieid"
+ ))?;
- Ok(Credentials { storage, keys })
+ let storage = StorageCredentials {
+ k2v_region: self.k2v_region.clone(),
+ s3_region: self.s3_region.clone(),
+ aws_access_key_id: user.aws_access_key_id.clone(),
+ aws_secret_access_key: user.aws_secret_access_key.clone(),
+ bucket,
+ };
+
+ let keys = match (&user.master_key, &user.secret_key) {
+ (Some(m), Some(s)) => {
+ let master_key =
+ Key::from_slice(&base64::decode(m)?).ok_or(anyhow!("Invalid master key"))?;
+ let secret_key = SecretKey::from_slice(&base64::decode(s)?)
+ .ok_or(anyhow!("Invalid secret key"))?;
+ CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await?
}
- }
+ (None, None) => {
+ let user_secrets = UserSecrets {
+ user_secret: user.user_secret.clone(),
+ alternate_user_secrets: user.alternate_user_secrets.clone(),
+ };
+ CryptoKeys::open(&storage, &user_secrets, password).await?
+ }
+ _ => bail!(
+ "Either both master and secret key or none of them must be specified for user"
+ ),
+ };
+
+ Ok(Credentials { storage, keys })
+ }
+
+ async fn public_login(&self, email: &str) -> Result<PublicCredentials> {
+ let user = match self.users_by_email.get(email) {
+ None => bail!("No user for email address {}", email),
+ Some(u) => u,
+ };
+
+ let bucket = user
+ .bucket
+ .clone()
+ .or_else(|| self.default_bucket.clone())
+ .ok_or(anyhow!(
+ "No bucket configured and no default bucket specieid"
+ ))?;
+
+ let storage = StorageCredentials {
+ k2v_region: self.k2v_region.clone(),
+ s3_region: self.s3_region.clone(),
+ aws_access_key_id: user.aws_access_key_id.clone(),
+ aws_secret_access_key: user.aws_secret_access_key.clone(),
+ bucket,
+ };
+
+ let k2v_client = storage.k2v_client()?;
+ let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?;
+
+ Ok(PublicCredentials {
+ storage,
+ public_key,
+ })
}
}