use apiplant_auth::{Principal, Session};
use ntex::web::types::{Json, State};
use ntex::web::{HttpRequest, HttpResponse};
use serde_json::{json, Value};
use uuid::Uuid;
use crate::auth_routes::table;
use crate::response::{db_error, error};
use crate::state::AppState;
fn session_response(session: Session) -> Value {
json!({
"user_id": session.user_id.to_string(),
"impersonator": session.impersonator.map(|id| id.to_string()),
"organization_id": session.org_lock.map(|id| id.to_string()),
})
}
pub async fn start(req: HttpRequest, state: State<AppState>, body: Json<Value>) -> HttpResponse {
let Some(principal) = state.resolve_principal(&req).await else {
return error(401, "authentication required");
};
if principal.is_impersonating() {
return error(409, "you are already acting as somebody else — stop first");
}
let Some(target) = body.get("user_id").and_then(|v| v.as_str()) else {
return error(400, "`user_id` is required");
};
let Ok(target) = Uuid::parse_str(target.trim()) else {
return error(400, "`user_id` is not a valid id");
};
if target == principal.user_id {
return error(400, "you are already yourself");
}
let session = match authorize(&state, &req, &principal, target).await {
Ok(session) => session,
Err(resp) => return resp,
};
match user_exists(&state, target).await {
Ok(true) => {}
Ok(false) => return error(404, "no such user"),
Err(resp) => return resp,
}
let Ok(token) = state.auth.issue_session(session) else {
return error(500, "could not issue a token");
};
tracing::info!(
actor = %principal.user_id,
subject = %target,
organization = ?session.org_lock,
"impersonation started"
);
let mut response = session_response(session);
response["token"] = Value::String(token);
HttpResponse::Ok().json(&response)
}
pub async fn stop(req: HttpRequest, state: State<AppState>) -> HttpResponse {
let Some(principal) = state.resolve_principal(&req).await else {
return error(401, "authentication required");
};
let Some(actor) = principal.impersonator else {
return error(400, "you are not acting as anybody else");
};
match user_exists(&state, actor).await {
Ok(true) => {}
Ok(false) => return error(401, "your own account no longer exists"),
Err(resp) => return resp,
}
let session = Session::plain(actor);
let Ok(token) = state.auth.issue_session(session) else {
return error(500, "could not issue a token");
};
tracing::info!(actor = %actor, subject = %principal.user_id, "impersonation ended");
let mut response = session_response(session);
response["token"] = Value::String(token);
HttpResponse::Ok().json(&response)
}
async fn authorize(
state: &AppState,
req: &HttpRequest,
principal: &Principal,
target: Uuid,
) -> Result<Session, HttpResponse> {
if state.is_global_admin(Some(principal)) {
return Ok(Session {
user_id: target,
impersonator: Some(principal.user_id),
org_lock: None,
});
}
if !state.app.config.auth.allow_impersonation {
return Err(error(403, "impersonation is switched off"));
}
let Some(org) = state.active_org(req, &Some(principal.clone())) else {
return Err(error(
403,
"select an organisation with the X-Organization header",
));
};
if !principal.is_admin_of(org) {
return Err(error(
403,
"only an admin of this organisation may act as one of its members",
));
}
if !state.organization_user_ids(org).await.contains(&target) {
return Err(error(
403,
"that person is not a member of this organisation",
));
}
Ok(Session {
user_id: target,
impersonator: Some(principal.user_id),
org_lock: Some(org),
})
}
async fn user_exists(state: &AppState, id: Uuid) -> Result<bool, HttpResponse> {
let Some(user_tbl) = table(state, "user") else {
return Err(error(500, "missing user resource"));
};
let sql = format!("SELECT id::text AS id FROM {user_tbl} WHERE id = $1::uuid LIMIT 1");
match state
.db
.raw_json(&sql, &[Value::String(id.to_string())])
.await
{
Ok(rows) => Ok(rows.as_array().is_some_and(|rows| !rows.is_empty())),
Err(e) => Err(db_error(e)),
}
}