use crate::server::OrdinaryApiServerState;
use anyhow::bail;
use axum::http::{HeaderMap, StatusCode};
use ordinary_utils::get_bearer_token_as_bytes;
use saferlmdb::{Database, DatabaseOptions, Environment, ReadTransaction, WriteTransaction, put};
use std::sync::Arc;
pub struct AccountLockManager {
env: Arc<Environment>,
lock_db: Arc<Database<'static>>,
}
impl AccountLockManager {
pub fn new(env: &Arc<Environment>) -> anyhow::Result<Self> {
let lock_db = Arc::new(Database::open(
env.clone(),
Some("locked"),
&DatabaseOptions::new(saferlmdb::db::Flags::CREATE),
)?);
Ok(Self {
env: env.clone(),
lock_db,
})
}
pub fn lock_account(&self, account: &str) -> anyhow::Result<()> {
if account == "root" {
bail!("cannot lock the root account");
}
let txn = WriteTransaction::new(self.env.clone())?;
{
let mut access = txn.access();
access.put(
&self.lock_db,
account.as_bytes(),
&[0u8],
&put::Flags::empty(),
)?;
}
txn.commit()?;
Ok(())
}
pub fn unlock_account(&self, account: &str) -> anyhow::Result<()> {
let txn = WriteTransaction::new(self.env.clone())?;
{
let mut access = txn.access();
access.del_key(&self.lock_db, account.as_bytes())?;
}
txn.commit()?;
Ok(())
}
pub fn is_locked(&self, account: &str) -> anyhow::Result<bool> {
let txn = ReadTransaction::new(self.env.clone())?;
let access = txn.access();
let is_locked = access
.get::<[u8], [u8]>(&self.lock_db, account.as_bytes())
.is_ok();
Ok(is_locked)
}
}
pub(crate) fn check(
state: &OrdinaryApiServerState,
headers: &HeaderMap,
permission: u8,
domain: &str,
) -> Result<String, StatusCode> {
if let Ok(token) = get_bearer_token_as_bytes(headers) {
match state.auth.verify_access_token(token.as_ref()) {
Ok((account, claims)) => {
let span = tracing::info_span!("permission", account);
span.in_scope(|| {
if account == "root" {
return Ok(account.to_string());
}
if domain == "root" {
tracing::error!("root-only operation disallowed");
return Err(StatusCode::UNAUTHORIZED);
}
let check_domain = claims.idx(1).as_str();
if check_domain != domain {
tracing::error!(domain, "token domain '{check_domain}' does not match");
return Err(StatusCode::UNAUTHORIZED);
}
let permissions = claims.idx(2).as_vector();
for claim_permission in &permissions {
if claim_permission.as_u8() == 0 || claim_permission.as_u8() == permission {
tracing::info!(
role = match claim_permission.as_u8() {
0 => "admin",
1 => "read",
2 => "write",
3 => "update",
4 => "upload",
5 => "install",
6 => "deploy",
7 => "bridge",
8 => "kill",
9 => "erase",
_ => "invalid",
},
"granted"
);
match state.account_lock_manager.is_locked(account) {
Ok(is_locked) => {
if is_locked {
tracing::error!(%account, "locked");
return Err(StatusCode::UNAUTHORIZED);
}
}
Err(err) => {
tracing::error!(%err);
return Err(StatusCode::UNAUTHORIZED);
}
}
return Ok(account.to_string());
}
}
tracing::error!(
permission = match permission {
0 => "admin",
1 => "read",
2 => "write",
3 => "update",
4 => "upload",
5 => "install",
6 => "deploy",
7 => "bridge",
8 => "kill",
9 => "erase",
_ => "invalid",
},
"not in token permissions"
);
Err(StatusCode::UNAUTHORIZED)
})
}
Err(err) => {
tracing::error!(%err, "access denied");
Err(StatusCode::UNAUTHORIZED)
}
}
} else {
tracing::error!("invalid token");
Err(StatusCode::UNAUTHORIZED)
}
}