use http::StatusCode;
use std::sync::Arc;
#[cfg(feature = "wiki")]
use crate::wiki::WikiAccess;
use crate::{
payload::AppError,
space::{AppState, Space},
types::{CWToken, SpaceToken, TokenScope},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthzMode {
PublicRead,
PublicReadLenient,
Credentialed,
CwtOnly,
}
pub struct Caller {
pub cwt: Option<CWToken>,
pub st: Option<SpaceToken>,
}
impl Caller {
#[cfg(feature = "wiki")]
pub fn actor(&self) -> String {
wiki_actor(&self.cwt, self.st.as_ref())
}
#[cfg(feature = "wiki")]
pub fn wiki_access(&self) -> WikiAccess {
wiki_read_access(&self.cwt, self.st.as_ref())
}
pub fn label_restricted(&self) -> bool {
label_restricted(self.st.as_ref())
}
pub fn recall_forbidden(&self) -> Option<&'static str> {
if self.label_restricted() {
Some("recall requires an unrestricted token")
} else {
None
}
}
pub fn conversation_read_forbidden(&self, collection: Option<&str>) -> Option<&'static str> {
conversation_read_forbidden(&self.cwt, self.st.as_ref(), collection)
}
}
#[derive(Debug)]
pub enum AuthzError {
ShardMismatch {
sharding: u32,
expected: u32,
},
Unauthorized(TokenScope),
SpaceNotFound {
space_id: String,
display: String,
debug: String,
},
SpaceLoad {
space_id: String,
display: String,
debug: String,
},
Forbidden(&'static str),
}
impl From<AuthzError> for AppError {
fn from(err: AuthzError) -> Self {
match err {
AuthzError::ShardMismatch { sharding, expected } => AppError::bad_request(format!(
"space_id sharding {} does not match server sharding {}",
sharding, expected
)),
AuthzError::Unauthorized(_) => AppError::unauthorized(),
AuthzError::SpaceNotFound {
space_id, debug, ..
} => {
let space_id = space_id.as_str();
log::warn!(target: "brain", space_id; "failed to load space: {debug}");
AppError::with_status(StatusCode::NOT_FOUND, "space not found")
}
AuthzError::SpaceLoad {
space_id, debug, ..
} => {
let space_id = space_id.as_str();
log::warn!(target: "brain", space_id; "failed to load space: {debug}");
AppError::with_status(StatusCode::INTERNAL_SERVER_ERROR, "failed to load space")
}
AuthzError::Forbidden(message) => AppError::with_status(StatusCode::FORBIDDEN, message),
}
}
}
pub fn ensure_sharding(app: &AppState, sharding: u32) -> Result<(), AuthzError> {
if sharding != app.sharding {
return Err(AuthzError::ShardMismatch {
sharding,
expected: app.sharding,
});
}
Ok(())
}
pub fn check_cwt(
app: &AppState,
space_id: &str,
token: &str,
scope: TokenScope,
now_ms: u64,
) -> Result<CWToken, AuthzError> {
app.check_auth(token, space_id, scope, now_ms)
.map_err(|_| AuthzError::Unauthorized(scope))
}
pub async fn load_space(app: &AppState, space_id: &str) -> Result<Arc<Space>, AuthzError> {
app.load_space(space_id, false).await.map_err(|err| {
let not_found = matches!(
err.downcast_ref::<anda_db::error::DBError>(),
Some(anda_db::error::DBError::NotFound { .. })
);
let space_id = space_id.to_string();
let display = err.to_string();
let debug = format!("{err:?}");
if not_found {
AuthzError::SpaceNotFound {
space_id,
display,
debug,
}
} else {
AuthzError::SpaceLoad {
space_id,
display,
debug,
}
}
})
}
pub async fn authorize(
app: &AppState,
space_id: &str,
token: &str,
sharding: Option<u32>,
scope: TokenScope,
mode: AuthzMode,
now_ms: u64,
) -> Result<(Arc<Space>, Caller), AuthzError> {
if let Some(sharding) = sharding {
ensure_sharding(app, sharding)?;
}
let cwt = match mode {
AuthzMode::CwtOnly => Some(check_cwt(app, space_id, token, scope, now_ms)?),
_ => app
.check_auth_if(token, space_id, scope, now_ms)
.map_err(|_| AuthzError::Unauthorized(scope))?,
};
let space = load_space(app, space_id).await?;
let st = match mode {
AuthzMode::CwtOnly => None,
_ if cwt.is_some() => None,
AuthzMode::Credentialed => Some(
space
.verify_space_token(token.to_string(), scope, now_ms)
.map_err(|_| AuthzError::Unauthorized(scope))?,
),
AuthzMode::PublicRead => {
match space.verify_space_token(token.to_string(), scope, now_ms) {
Ok(st) => Some(st),
Err(_) if space.is_public() => None,
Err(_) => return Err(AuthzError::Unauthorized(scope)),
}
}
AuthzMode::PublicReadLenient => {
if space.is_public() {
None
} else {
Some(
space
.verify_space_token(token.to_string(), scope, now_ms)
.map_err(|_| AuthzError::Unauthorized(scope))?,
)
}
}
};
Ok((space, Caller { cwt, st }))
}
#[cfg(feature = "wiki")]
pub fn wiki_actor(t: &Option<CWToken>, st: Option<&SpaceToken>) -> String {
if let Some(t) = t {
return t.user.to_string();
}
match st {
Some(st) if !st.name.trim().is_empty() => format!("st:{}", st.name.trim()),
Some(_) => "st:unnamed".to_string(),
None => "anonymous".to_string(),
}
}
pub fn label_restricted(st: Option<&SpaceToken>) -> bool {
st.is_some_and(|st| st.labels.is_some())
}
pub fn conversation_read_forbidden(
t: &Option<CWToken>,
st: Option<&SpaceToken>,
collection: Option<&str>,
) -> Option<&'static str> {
if label_restricted(st) {
return Some("conversations require an unrestricted token");
}
if collection == Some("recall") && t.is_none() && st.is_none() {
return Some(
"recall conversations require a credential; anonymous public access is denied",
);
}
None
}
#[cfg(feature = "wiki")]
pub fn wiki_read_access(t: &Option<CWToken>, st: Option<&SpaceToken>) -> WikiAccess {
let labels = if t.is_some() {
None
} else if let Some(st) = st {
st.labels.clone()
} else {
Some(Vec::new())
};
WikiAccess {
actor: wiki_actor(t, st),
labels,
}
}