use std::sync::Arc;
use axum::Json;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use sqlx::{Pool, Sqlite};
use crate::auth::AuthContext;
use crate::moderators::Role;
use crate::writer::WriterHandle;
use super::AdminConfig;
#[derive(Debug, Clone)]
pub(super) struct VerifiedAdmin {
pub caller_did: String,
pub role: Role,
#[allow(dead_code)]
pub key_id: String,
}
#[derive(Clone)]
pub(super) struct AdminState {
pub pool: Pool<Sqlite>,
pub writer: WriterHandle,
pub auth: Arc<AuthContext>,
pub config: Arc<AdminConfig>,
pub strike_policy: Arc<crate::moderation::policy::StrikePolicy>,
}
#[derive(Debug)]
pub(super) enum AdminError {
AuthenticationRequired,
Forbidden,
InvalidRequest(&'static str),
InvalidRequestOwned(String),
LabelNotFound,
ReportNotFound,
AuditEntryNotFound,
InvalidLabelValue,
InvalidReason,
InvalidActionType,
DurationRequired,
DurationNotAllowed,
SubjectUriMismatch,
ActionNotFound,
ActionAlreadyRevoked,
PendingActionNotFound,
PendingAlreadyResolved,
SubjectTakendown,
SubjectNotFound,
Internal,
}
#[derive(Serialize)]
struct ErrorBody {
error: &'static str,
message: &'static str,
}
#[derive(Serialize)]
struct OwnedErrorBody {
error: &'static str,
message: String,
}
impl IntoResponse for AdminError {
fn into_response(self) -> Response {
if let AdminError::InvalidRequestOwned(msg) = self {
return (
StatusCode::BAD_REQUEST,
Json(OwnedErrorBody {
error: "InvalidRequest",
message: msg,
}),
)
.into_response();
}
let (status, body) = match self {
AdminError::InvalidRequestOwned(_) => unreachable!("handled above"),
AdminError::AuthenticationRequired => (
StatusCode::UNAUTHORIZED,
ErrorBody {
error: "AuthenticationRequired",
message: "authentication required",
},
),
AdminError::Forbidden => (
StatusCode::FORBIDDEN,
ErrorBody {
error: "Forbidden",
message: "forbidden",
},
),
AdminError::InvalidRequest(msg) => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "InvalidRequest",
message: msg,
},
),
AdminError::LabelNotFound => (
StatusCode::NOT_FOUND,
ErrorBody {
error: "LabelNotFound",
message: "no applied label for the given tuple",
},
),
AdminError::ReportNotFound => (
StatusCode::NOT_FOUND,
ErrorBody {
error: "ReportNotFound",
message: "report not found",
},
),
AdminError::AuditEntryNotFound => (
StatusCode::NOT_FOUND,
ErrorBody {
error: "AuditEntryNotFound",
message: "audit entry not found",
},
),
AdminError::InvalidLabelValue => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "InvalidLabelValue",
message: "label value not accepted by this labeler",
},
),
AdminError::InvalidReason => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "InvalidReason",
message: "reason identifier not declared in [moderation_reasons]",
},
),
AdminError::InvalidActionType => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "InvalidActionType",
message: "type must be one of warning / note / temp_suspension / indef_suspension / takedown",
},
),
AdminError::DurationRequired => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "DurationRequired",
message: "duration is required for type=temp_suspension",
},
),
AdminError::DurationNotAllowed => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "DurationNotAllowed",
message: "duration is only valid for type=temp_suspension",
},
),
AdminError::SubjectUriMismatch => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "SubjectUriMismatch",
message: "subject URI repo does not match subject_did",
},
),
AdminError::ActionNotFound => (
StatusCode::NOT_FOUND,
ErrorBody {
error: "ActionNotFound",
message: "subject action not found",
},
),
AdminError::ActionAlreadyRevoked => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "ActionAlreadyRevoked",
message: "subject action is already revoked",
},
),
AdminError::PendingActionNotFound => (
StatusCode::NOT_FOUND,
ErrorBody {
error: "PendingActionNotFound",
message: "pending policy action not found",
},
),
AdminError::PendingAlreadyResolved => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "PendingAlreadyResolved",
message: "pending policy action is already resolved",
},
),
AdminError::SubjectTakendown => (
StatusCode::BAD_REQUEST,
ErrorBody {
error: "SubjectTakendown",
message: "subject is already taken down; confirm not permitted",
},
),
AdminError::SubjectNotFound => (
StatusCode::NOT_FOUND,
ErrorBody {
error: "SubjectNotFound",
message: "no actions recorded for subject",
},
),
AdminError::Internal => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorBody {
error: "InternalServerError",
message: "service temporarily unavailable",
},
),
};
(status, Json(body)).into_response()
}
}
pub(super) async fn verify_and_authorize(
state: &AdminState,
headers: &HeaderMap,
lxm: &str,
) -> Result<VerifiedAdmin, AdminError> {
if headers.contains_key("origin") {
return Err(AdminError::Forbidden);
}
let token = headers
.get("authorization")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.ok_or(AdminError::AuthenticationRequired)?;
let caller = state
.auth
.verify_service_auth(token, lxm)
.await
.map_err(|_| AdminError::AuthenticationRequired)?;
let row = sqlx::query!("SELECT role FROM moderators WHERE did = ?1", caller.iss)
.fetch_optional(&state.pool)
.await
.map_err(|_| AdminError::Internal)?;
let role = match row.as_ref().map(|r| r.role.as_str()) {
Some("admin") => Role::Admin,
Some("mod") => Role::Mod,
Some(_) => return Err(AdminError::Internal), None => return Err(AdminError::Forbidden),
};
Ok(VerifiedAdmin {
caller_did: caller.iss,
role,
key_id: caller.key_id,
})
}
pub(super) async fn verify_and_authorize_admin_only(
state: &AdminState,
headers: &HeaderMap,
lxm: &str,
) -> Result<VerifiedAdmin, AdminError> {
let v = verify_and_authorize(state, headers, lxm).await?;
if v.role != Role::Admin {
return Err(AdminError::Forbidden);
}
Ok(v)
}