Implement public_login
This commit is contained in:
parent
8192d062ba
commit
d53cf1d220
4 changed files with 195 additions and 75 deletions
|
@ -23,6 +23,8 @@ pub struct LoginStaticConfig {
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct LoginStaticUser {
|
pub struct LoginStaticUser {
|
||||||
|
#[serde(default)]
|
||||||
|
pub email_addresses: Vec<String>,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
pub aws_access_key_id: String,
|
pub aws_access_key_id: String,
|
||||||
|
|
|
@ -84,11 +84,30 @@ impl LdapLoginProvider {
|
||||||
bucket_source,
|
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]
|
#[async_trait]
|
||||||
impl LoginProvider for LdapLoginProvider {
|
impl LoginProvider for LdapLoginProvider {
|
||||||
async fn login(&self, username: &str, password: &str) -> Result<Credentials> {
|
async fn login(&self, username: &str, password: &str) -> Result<Credentials> {
|
||||||
|
check_identifier(username)?;
|
||||||
|
|
||||||
let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?;
|
let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?;
|
||||||
ldap3::drive!(conn);
|
ldap3::drive!(conn);
|
||||||
|
|
||||||
|
@ -97,13 +116,6 @@ impl LoginProvider for LdapLoginProvider {
|
||||||
ldap.simple_bind(dn, pw).await?.success()?;
|
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
|
let (matches, _res) = ldap
|
||||||
.search(
|
.search(
|
||||||
&self.search_base,
|
&self.search_base,
|
||||||
|
@ -137,32 +149,9 @@ impl LoginProvider for LdapLoginProvider {
|
||||||
.context("Invalid password")?;
|
.context("Invalid password")?;
|
||||||
debug!("Ldap login with user name {} successfull", username);
|
debug!("Ldap login with user name {} successfull", username);
|
||||||
|
|
||||||
let get_attr = |attr: &str| -> Result<String> {
|
let storage = self.storage_creds_from_ldap_user(&user)?;
|
||||||
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 = StorageCredentials {
|
let user_secret = get_attr(&user, &self.user_secret_attr)?;
|
||||||
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 alternate_user_secrets = match &self.alternate_user_secrets_attr {
|
let alternate_user_secrets = match &self.alternate_user_secrets_attr {
|
||||||
None => vec![],
|
None => vec![],
|
||||||
Some(a) => user.attrs.get(a).cloned().unwrap_or_default(),
|
Some(a) => user.attrs.get(a).cloned().unwrap_or_default(),
|
||||||
|
@ -178,4 +167,71 @@ impl LoginProvider for LdapLoginProvider {
|
||||||
|
|
||||||
Ok(Credentials { storage, keys })
|
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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,9 @@ pub trait LoginProvider {
|
||||||
/// The login method takes an account's password as an input to decypher
|
/// The login method takes an account's password as an input to decypher
|
||||||
/// decryption keys and obtain full access to the user's account.
|
/// decryption keys and obtain full access to the user's account.
|
||||||
async fn login(&self, username: &str, password: &str) -> Result<Credentials>;
|
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
|
/// The struct Credentials represent all of the necessary information to interact
|
||||||
|
@ -36,6 +39,13 @@ pub struct Credentials {
|
||||||
pub keys: CryptoKeys,
|
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
|
/// The struct StorageCredentials contains access key to an S3 and K2V bucket
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct StorageCredentials {
|
pub struct StorageCredentials {
|
||||||
|
@ -396,7 +406,7 @@ impl CryptoKeys {
|
||||||
Ok((salt_ct, public_ct))
|
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
|
let mut params = k2v
|
||||||
.read_batch(&[
|
.read_batch(&[
|
||||||
k2v_read_single_key("keys", "salt", false),
|
k2v_read_single_key("keys", "salt", false),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -10,16 +11,34 @@ use crate::login::*;
|
||||||
|
|
||||||
pub struct StaticLoginProvider {
|
pub struct StaticLoginProvider {
|
||||||
default_bucket: Option<String>,
|
default_bucket: Option<String>,
|
||||||
users: HashMap<String, LoginStaticUser>,
|
users: HashMap<String, Arc<LoginStaticUser>>,
|
||||||
|
users_by_email: HashMap<String, Arc<LoginStaticUser>>,
|
||||||
|
|
||||||
k2v_region: Region,
|
k2v_region: Region,
|
||||||
s3_region: Region,
|
s3_region: Region,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticLoginProvider {
|
impl StaticLoginProvider {
|
||||||
pub fn new(config: LoginStaticConfig, k2v_region: Region, s3_region: Region) -> Result<Self> {
|
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 {
|
Ok(Self {
|
||||||
default_bucket: config.default_bucket,
|
default_bucket: config.default_bucket,
|
||||||
users: config.users,
|
users,
|
||||||
|
users_by_email,
|
||||||
k2v_region,
|
k2v_region,
|
||||||
s3_region,
|
s3_region,
|
||||||
})
|
})
|
||||||
|
@ -29,49 +48,82 @@ impl StaticLoginProvider {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LoginProvider for StaticLoginProvider {
|
impl LoginProvider for StaticLoginProvider {
|
||||||
async fn login(&self, username: &str, password: &str) -> Result<Credentials> {
|
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),
|
None => bail!("User {} does not exist", username),
|
||||||
Some(u) => {
|
Some(u) => 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 {
|
if !verify_password(password, &user.password)? {
|
||||||
k2v_region: self.k2v_region.clone(),
|
bail!("Wrong password");
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
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"),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Credentials { storage, keys })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue