upub/routes/src/auth.rs

171 lines
5.2 KiB
Rust
Raw Normal View History

use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts}};
2024-03-25 01:58:30 +01:00
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
use httpsign::HttpSignature;
use upub::traits::{fetch::RequestError, Fetcher};
2024-03-25 01:58:30 +01:00
use crate::ApiError;
2024-03-25 01:58:30 +01:00
#[derive(Debug, Clone)]
pub enum Identity {
Anonymous,
Remote {
user: String,
domain: String,
internal: i64,
},
Local {
id: String,
internal: i64,
},
2024-03-25 01:58:30 +01:00
}
impl Identity {
// TODO i hate having to do this, but if i don't include `activity.actor` column
// we can't see our own activities (likes, announces, follows), and if i do
// all queries which don't bring up activities break. so it's necessary to
// split these two in order to manually include the extra filter when
// needed
pub fn filter_objects(&self) -> Condition {
let base_cond = Condition::any().add(upub::model::addressing::Column::Actor.is_null());
match self {
Identity::Anonymous => base_cond,
Identity::Remote { internal, .. } => base_cond.add(upub::model::addressing::Column::Instance.eq(*internal)),
Identity::Local { internal, id } => base_cond
.add(upub::model::addressing::Column::Actor.eq(*internal))
.add(upub::model::object::Column::AttributedTo.eq(id))
}
}
pub fn filter_activities(&self) -> Condition {
2024-06-08 02:18:45 +02:00
let base_cond = Condition::any().add(upub::model::addressing::Column::Actor.is_null());
match self {
Identity::Anonymous => base_cond,
Identity::Remote { internal, .. } => base_cond.add(upub::model::addressing::Column::Instance.eq(*internal)),
Identity::Local { internal, id } => base_cond
.add(upub::model::addressing::Column::Actor.eq(*internal))
2024-12-10 22:47:44 +01:00
.add(upub::model::object::Column::AttributedTo.eq(id))
.add(upub::model::activity::Column::Actor.eq(id)),
2024-06-08 02:18:45 +02:00
}
}
pub fn my_id(&self) -> Option<i64> {
match self {
Identity::Local { internal, .. } => Some(*internal),
_ => None,
}
}
pub fn is(&self, uid: &str) -> bool {
match self {
Identity::Anonymous => false,
Identity::Remote { .. } => false, // TODO per-actor server auth should check this
Identity::Local { id, .. } => id.as_str() == uid
}
}
2024-05-27 16:58:51 +02:00
#[allow(unused)]
pub fn is_anon(&self) -> bool {
matches!(self, Self::Anonymous)
}
2024-05-27 16:58:51 +02:00
#[allow(unused)]
pub fn is_local(&self) -> bool {
matches!(self, Self::Local { .. })
}
2024-05-27 16:58:51 +02:00
#[allow(unused)]
pub fn is_remote(&self) -> bool {
matches!(self, Self::Remote { .. })
}
}
2024-03-25 01:58:30 +01:00
pub struct AuthIdentity(pub Identity);
#[axum::async_trait]
2024-03-25 01:58:30 +01:00
impl<S> FromRequestParts<S> for AuthIdentity
where
upub::Context: FromRef<S>,
2024-03-25 01:58:30 +01:00
S: Send + Sync,
{
type Rejection = ApiError;
2024-03-25 01:58:30 +01:00
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let ctx = upub::Context::from_ref(state);
2024-03-25 01:58:30 +01:00
let mut identity = Identity::Anonymous;
let auth_header = parts
.headers
.get(header::AUTHORIZATION)
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("");
if auth_header.starts_with("Bearer ") {
match upub::model::session::Entity::find()
.filter(upub::model::session::Column::Secret.eq(auth_header.replace("Bearer ", "")))
.filter(upub::model::session::Column::Expires.gt(chrono::Utc::now()))
2024-03-25 01:58:30 +01:00
.one(ctx.db())
.await?
2024-03-25 01:58:30 +01:00
{
None => return Err(ApiError::unauthorized()),
Some(x) => {
// TODO could we store both actor ap id and internal id in session? to avoid this extra
// lookup on *every* local authed request we receive...
let internal = upub::model::actor::Entity::ap_to_internal(&x.actor, ctx.db())
.await?
.ok_or_else(ApiError::internal_server_error)?;
identity = Identity::Local { id: x.actor, internal };
},
2024-03-25 01:58:30 +01:00
}
}
2024-04-13 01:49:23 +02:00
if let Some(sig) = parts
.headers
.get("Signature")
.map(|v| v.to_str().unwrap_or(""))
{
tracing::debug!("validating http signature '{sig}'");
2024-04-13 21:22:19 +02:00
let mut http_signature = HttpSignature::parse(sig);
2024-04-13 21:22:19 +02:00
// TODO assert payload's digest is equal to signature's
// really annoying to do here because we're streaming
// the request, maybe even impossible with this design?
2024-04-16 19:19:49 +02:00
let user_id = http_signature.key_id
.replace("/main-key", "") // gotosocial whyyyyy
2024-04-16 19:19:49 +02:00
.split('#')
.next().ok_or(ApiError::bad_request())?
2024-04-16 19:19:49 +02:00
.to_string();
2024-04-13 21:22:19 +02:00
match ctx.fetch_user(&user_id, ctx.db()).await {
Err(RequestError::Database(x)) => return Err(RequestError::Database(x).into()),
Err(e) => tracing::debug!("could not fetch {user_id} to verify signature: {e}"),
Ok(user) => {
let signature = http_signature.build_from_parts(parts);
tracing::debug!("constructed http signature {signature:?}");
let valid = signature.verify(&user.public_key)?;
if !valid {
tracing::warn!("refusing mismatching http signature");
return Err(ApiError::unauthorized());
}
if ctx.cfg().reject.requests.contains(&user.domain) {
return Err(ApiError::Status(axum::http::StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS));
}
if !ctx.cfg().reject.access.contains(&user.domain) {
let internal = upub::model::instance::Entity::domain_to_internal(&user.domain, ctx.db())
.await?
.ok_or_else(ApiError::internal_server_error)?; // user but not their domain???
identity = Identity::Remote { user: user.id, domain: user.domain, internal };
}
},
}
2024-04-13 01:49:23 +02:00
}
2024-03-25 01:58:30 +01:00
Ok(AuthIdentity(identity))
}
}