Aldonu aŭtentigo per pasvorto

Tio postulas kampojn por la pasvorto kaj la salo en la datumbazo.
This commit is contained in:
Savanni D'Gerinel 2022-01-16 12:07:10 -05:00
parent 823a701a2f
commit f4164a62c4
5 changed files with 344 additions and 48 deletions

46
Cargo.lock generated
View File

@ -46,6 +46,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
dependencies = [
"generic-array",
]
[[package]]
name = "buf_redux"
version = "0.8.4"
@ -92,6 +101,15 @@ dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0"
dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.9.0"
@ -101,6 +119,17 @@ dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b"
dependencies = [
"block-buffer 0.10.0",
"crypto-common",
"generic-array",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@ -403,7 +432,9 @@ name = "kampanja-kontrolada-servilo"
version = "0.1.0"
dependencies = [
"anyhow",
"rand 0.8.4",
"rusqlite",
"sha2",
"tempfile",
"thiserror",
"tokio",
@ -833,13 +864,24 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
dependencies = [
"block-buffer",
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"digest",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.1",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"

View File

@ -7,7 +7,9 @@ edition = "2018"
[dependencies]
anyhow = { version = "1" }
rand = { version = "0.8" }
rusqlite = { version = "0.26" }
sha2 = { version = "0.10" }
thiserror = { version = "1" }
tokio = { version = "1", features = ["full"] }
uuid = { version = "0.8", features = ["v4"] }

View File

@ -1,5 +1,11 @@
use crate::datumbazo::Datumbazo;
use rusqlite::OptionalExtension;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rusqlite::{
params,
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
OptionalExtension, ToSql,
};
use sha2::{Digest, Sha256};
use std::{
collections::HashMap,
convert::Infallible,
@ -10,24 +16,24 @@ use uuid::{adapter::Hyphenated, Uuid};
use warp::{reject, reject::Reject, Filter, Rejection};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Ĵetono(String);
pub struct SesiaĴetono(String);
impl From<&str> for Ĵetono {
impl From<&str> for SesiaĴetono {
fn from(s: &str) -> Self {
Ĵetono(s.to_owned())
SesiaĴetono(s.to_owned())
}
}
impl FromStr for Ĵetono {
impl FromStr for SesiaĴetono {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Ĵetono(s.to_owned()))
Ok(SesiaĴetono(s.to_owned()))
}
}
impl From<Ĵetono> for String {
fn from(s: Ĵetono) -> Self {
impl From<SesiaĴetono> for String {
fn from(s: SesiaĴetono) -> Self {
s.0.clone()
}
}
@ -47,6 +53,17 @@ impl From<UzantIdentigilo> for String {
}
}
impl FromSql for UzantIdentigilo {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Text(t) => Ok(UzantIdentigilo::from(
String::from_utf8(Vec::from(t)).unwrap().as_ref(),
)),
_ => Err(FromSqlError::InvalidType),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Uzantnomo(String);
@ -56,47 +73,177 @@ impl From<&str> for Uzantnomo {
}
}
impl From<&Uzantnomo> for String {
fn from(s: &Uzantnomo) -> Self {
s.0.clone()
}
}
impl From<Uzantnomo> for String {
fn from(s: Uzantnomo) -> Self {
s.0.clone()
}
}
pub trait AŭtentigoDB: Send {
fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)>;
impl FromSql for Uzantnomo {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Text(t) => Ok(Uzantnomo::from(
String::from_utf8(Vec::from(t)).unwrap().as_ref(),
)),
_ => Err(FromSqlError::InvalidType),
}
}
}
fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Pasvorto(String);
impl Pasvorto {
fn haku(&self, salo: &Salo) -> HakitaPasvorto {
let mut haketilo = Sha256::new();
haketilo.update(String::from(self).as_bytes());
haketilo.update(salo);
HakitaPasvorto(Vec::from(haketilo.finalize().as_slice()))
}
}
impl From<&str> for Pasvorto {
fn from(s: &str) -> Self {
Pasvorto(s.to_owned())
}
}
impl From<&Pasvorto> for String {
fn from(s: &Pasvorto) -> Self {
s.0.clone()
}
}
impl From<Pasvorto> for String {
fn from(s: Pasvorto) -> Self {
s.0.clone()
}
}
#[derive(PartialEq)]
struct HakitaPasvorto(Vec<u8>);
impl ToSql for HakitaPasvorto {
fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
Ok(ToSqlOutput::Borrowed(ValueRef::Blob(self.0.as_ref())))
}
}
impl FromSql for HakitaPasvorto {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Blob(t) => Ok(HakitaPasvorto(Vec::from(t))),
_ => Err(FromSqlError::InvalidType),
}
}
}
struct Salo(Vec<u8>);
impl AsRef<[u8]> for Salo {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl ToSql for Salo {
fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
Ok(ToSqlOutput::Borrowed(ValueRef::Blob(self.0.as_ref())))
}
}
impl FromSql for Salo {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Blob(t) => Ok(Salo(Vec::from(t))),
_ => Err(FromSqlError::InvalidType),
}
}
}
pub trait AŭtentigoDB: Send {
fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo;
fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono;
fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto);
fn aŭtentigu_per_pasvorto(
&self,
uzantnomo: &Uzantnomo,
pasvorto: &Pasvorto,
) -> Option<UzantIdentigilo>;
fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)>;
}
pub struct MemAŭtentigo {
sesioj: HashMap<Ĵetono, UzantIdentigilo>,
uzantoj: HashMap<UzantIdentigilo, Uzantnomo>,
inversa_uzantoj: HashMap<Uzantnomo, UzantIdentigilo>,
pasvortoj: HashMap<UzantIdentigilo, Pasvorto>,
sesioj: HashMap<SesiaĴetono, UzantIdentigilo>,
uzantoj: HashMap<UzantIdentigilo, Uzantnomo>,
}
impl MemAŭtentigo {
pub fn new() -> Self {
Self {
inversa_uzantoj: HashMap::new(),
pasvortoj: HashMap::new(),
sesioj: HashMap::new(),
uzantoj: HashMap::new(),
inversa_uzantoj: HashMap::new(),
}
}
}
impl AŭtentigoDB for MemAŭtentigo {
fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)> {
fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo {
let uzant_id =
UzantIdentigilo::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
self.uzantoj.insert(uzant_id.clone(), uzantnomo.clone());
self.pasvortoj.insert(uzant_id.clone(), pasvorto);
self.inversa_uzantoj.insert(uzantnomo, uzant_id.clone());
uzant_id
}
fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono {
let identigilo = self.inversa_uzantoj.get(&uzantnomo).cloned().unwrap();
let ĵetono =
SesiaĴetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
self.sesioj.insert(ĵetono.clone(), identigilo);
ĵetono
}
fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto) {
self.pasvortoj.insert(identigilo.clone(), pasvorto);
}
fn aŭtentigu_per_pasvorto(
&self,
uzantnomo: &Uzantnomo,
pasvorto: &Pasvorto,
) -> Option<UzantIdentigilo> {
self.inversa_uzantoj.get(&uzantnomo).and_then(|id| {
self.pasvortoj.get(id).and_then(|kandidato| {
if *pasvorto == *kandidato {
Some(id.clone())
} else {
None
}
})
})
}
fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)> {
let identigilo = self.sesioj.get(&ĵetono).cloned()?;
let uzantnomo = self.uzantoj.get(&identigilo).cloned()?;
Some((identigilo, uzantnomo))
}
fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono {
let identigilo = self.inversa_uzantoj.get(&uzantnomo).cloned().unwrap();
let ĵetono = Ĵetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
self.sesioj.insert(ĵetono.clone(), identigilo);
ĵetono
}
}
pub struct DBAŭtentigo {
@ -110,26 +257,50 @@ impl DBAŭtentigo {
}
impl AŭtentigoDB for DBAŭtentigo {
fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)> {
let konekto = self.pool.konektu().unwrap();
konekto
fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo {
let mut konekto = self.pool.konektu().unwrap();
let tr = konekto.transaction().unwrap();
let identigilo =
UzantIdentigilo::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
let cnt: usize = tr
.query_row(
"SELECT uzantoj.id, uzantoj.nomo FROM sesioj INNER JOIN uzantoj on sesioj.uzanto == uzantoj.id WHERE sesioj.id = ?",
[String::from(ĵetono)],
|row| {
let identigilo = row.get("id")
.map(|s: String| UzantIdentigilo::from(s.as_str())).unwrap();
let nomo = row.get("nomo")
.map(|s: String| Uzantnomo::from(s.as_str())).unwrap();
Ok((identigilo, nomo))
},
"SELECT count(*) from uzantoj WHERE nomo = ?",
params![String::from(uzantnomo.clone())],
|row| row.get("count(*)"),
)
.optional()
.unwrap()
.unwrap();
if cnt > 0 {
panic!("uzanto jam ekzistas");
} else {
let salo = Salo(
thread_rng()
.sample_iter(&Alphanumeric)
.take(10)
.collect::<Vec<u8>>(),
);
let pasvorto = pasvorto.haku(&salo);
tr.execute(
"INSERT INTO uzantoj VALUES(?, ?, ?, ?)",
params![
String::from(identigilo.clone()),
String::from(uzantnomo),
pasvorto,
salo
],
)
.unwrap();
}
tr.commit().unwrap();
identigilo
}
fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono {
let ĵetono = Ĵetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono {
let ĵetono =
SesiaĴetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
let mut konekto = self.pool.konektu().unwrap();
let tr = konekto.transaction().unwrap();
let uzanta_id: Option<String> = tr
@ -153,6 +324,68 @@ impl AŭtentigoDB for DBAŭtentigo {
tr.commit().unwrap();
ĵetono
}
fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto) {}
fn aŭtentigu_per_pasvorto(
&self,
uzantnomo: &Uzantnomo,
pasvorto: &Pasvorto,
) -> Option<UzantIdentigilo> {
let konekto = self.pool.konektu().unwrap();
let mut demando = konekto
.prepare_cached("SELECT * FROM uzantoj WHERE nomo = ?")
.unwrap();
let rezultoj = demando.query_map(params![String::from(uzantnomo)], |row| {
let id = row.get("id")?;
let nomo = row.get("nomo")?;
let pasvorto = row.get("pasvorto")?;
let salo = row.get("pasvortsalo")?;
Ok((id, nomo, pasvorto, salo))
});
let rows: Vec<(UzantIdentigilo, Uzantnomo, HakitaPasvorto, Salo)> = match rezultoj {
Ok(r) => {
let mut rows = Vec::new();
for row in r {
let (id, nomo, pasvorto, salo) = row.unwrap();
rows.push((id, nomo, pasvorto, salo));
}
rows
}
Err(_) => panic!("eraro en datumbazo"),
};
match rows.len() {
1 => {
let (ref identigilo, _, ref hakita_pasvorto, ref salo) = rows[0];
if pasvorto.haku(&salo) == *hakita_pasvorto {
Some(identigilo.clone())
} else {
None
}
}
0 => None,
_ => panic!("pli ol unu kongruo trovis por uzantnomo"),
}
}
fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)> {
let konekto = self.pool.konektu().unwrap();
konekto.query_row(
"SELECT uzantoj.id, uzantoj.nomo FROM sesioj INNER JOIN uzantoj on sesioj.uzanto == uzantoj.id WHERE sesioj.id = ?",
params![String::from(ĵetono.clone())],
|row| {
let identigilo = row.get("id")
.map(|s: String| UzantIdentigilo::from(s.as_str())).unwrap();
let nomo = row.get("nomo")
.map(|s: String| Uzantnomo::from(s.as_str())).unwrap();
Ok((identigilo, nomo))
},
)
.optional()
.unwrap()
}
}
#[derive(Debug)]
@ -166,10 +399,10 @@ pub fn kun_aŭtentigo(
let auth_ctx = auth_ctx.clone();
warp::header("authentication").and_then({
let auth_ctx = auth_ctx.clone();
move |text| {
move |text: SesiaĴetono| {
let auth_ctx = auth_ctx.clone();
async move {
match auth_ctx.read().unwrap().aŭtentigu(text) {
match auth_ctx.read().unwrap().aŭtentigu_per_sesio(&text) {
Some(salutiloj) => Ok(salutiloj),
None => Err(reject::custom(AŭtentigoPostulas)),
}
@ -177,3 +410,22 @@ pub fn kun_aŭtentigo(
}
})
}
#[cfg(test)]
mod test {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn ĝi_povas_krei_uzanto() {
let vojo = NamedTempFile::new().unwrap().into_temp_path();
let datumbazo = Datumbazo::kreu(vojo.to_path_buf()).unwrap();
let mut aŭtentigo = DBAŭtentigo::kreu(datumbazo);
let identigilo =
aŭtentigo.kreu_uzanton(Uzantnomo::from("savanni"), Pasvorto::from("abcdefg"));
let aŭtentita_identigilo = aŭtentigo
.aŭtentigu_per_pasvorto(&Uzantnomo::from("savanni"), &Pasvorto::from("abcdefg"));
assert_eq!(aŭtentita_identigilo, Some(identigilo));
}
}

View File

@ -25,7 +25,7 @@ impl Datumbazo {
println!("versio: {}", versio);
if versio == 0 {
tx.execute_batch(
"CREATE TABLE uzantoj (id string primary key, nomo text);
"CREATE TABLE uzantoj (id string primary key, nomo text, pasvorto text, pasvortsalo text);
CREATE TABLE rajtoj (uzanto string, rajto string, foreign key(uzanto) references uzanto(id));
CREATE TABLE sesioj (id string primary key not null, uzanto string, foreign key(uzanto) references uzantoj(id));
PRAGMA user_version = 1;",
@ -97,20 +97,20 @@ mod test {
let mut konekto = datumbazo.konektu().unwrap();
let tr = konekto.transaction().unwrap();
tr.execute(
"INSERT INTO uzantoj VALUES(?, ?)",
params![1, String::from("mia-uzantnomo")],
"INSERT INTO uzantoj VALUES(?, ?, ?, ?)",
params!["abcdfeg", "mia-uzantnomo", "pasvorto", "abcdefg"],
)
.unwrap();
tr.commit().unwrap();
let konekto = datumbazo.konektu().unwrap();
let id: Option<u32> = konekto
let id: Option<String> = konekto
.query_row(
"SELECT id FROM uzantoj WHERE nomo = ?",
[String::from("mia-uzantnomo")],
|row| row.get("id"),
)
.unwrap();
assert_eq!(id, Some(1));
assert_eq!(id, Some(String::from("abcdfeg")));
}
}

View File

@ -54,7 +54,7 @@ pub async fn main() {
let ĵetono = auth_ctx
.write()
.unwrap()
.kreu_ĵetono(Uzantnomo::from("savanni"));
.kreu_sesion(&Uzantnomo::from("savanni"));
println!("ĵetono: {}", String::from(ĵetono));
let rajtigo = Arc::new(RwLock::new(DBRajtigo::kreu(db)));