pray-cli 1.10.0

Package manager for the language placed before inference — Prayfile CLI (binary: pray)
use crate::server::{response_with_status, Response};
use crate::server_auth_delivery::deliver_verification_code;
use pray_core::auth::{
    bearer_token_from_authorization, AuthPasskeyChallengeRequest, AuthPasskeyChallengeResponse,
    AuthPasskeyEnrollmentRequest, AuthPasskeyEnrollmentResponse, AuthPasskeyLoginRequest,
    AuthPasskeyLoginResponse, AuthRegistrationRequest, AuthRegistrationResponse,
    AuthSessionRequest, AuthSessionResponse, AuthSshKeyChallengeRequest,
    AuthSshKeyChallengeResponse, AuthSshKeyEnrollmentRequest, AuthSshKeyEnrollmentResponse,
    AuthSshKeyLoginRequest, AuthSshKeyLoginResponse, AuthVerificationRequest,
    AuthVerificationResponse, RegistryAuthStore,
};
use pray_core::trust::read_registry_trust_settings;
use pray_core::{PrayError, PrayResult};
use std::path::Path;

pub(crate) fn auth_register_response(root: &Path, body: &[u8]) -> PrayResult<Response> {
    let request: AuthRegistrationRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth registration",
            message: error.to_string(),
        })?;
    let trust = read_registry_trust_settings(root)?;
    let store = RegistryAuthStore::open(root)?;
    let response: AuthRegistrationResponse =
        store.register_email(&request.email, trust.email_confirmation)?;
    if let Some(code) = response.verification_code.as_deref() {
        deliver_verification_code(root, &response.email, code)?;
    }
    let body = serde_json::to_vec(&serde_json::json!({
        "email": response.email,
        "verified": response.verified,
    }))
    .map_err(|error| PrayError::Manifest(error.to_string()))?;
    Ok(Response {
        status: 201,
        content_type: "application/json".to_string(),
        body,
    })
}

pub(crate) fn auth_verify_response(root: &Path, body: &[u8]) -> PrayResult<Response> {
    let request: AuthVerificationRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth verification",
            message: error.to_string(),
        })?;
    let store = RegistryAuthStore::open(root)?;
    let response: AuthVerificationResponse = store.verify_email(&request.email, &request.code)?;
    let body =
        serde_json::to_vec(&response).map_err(|error| PrayError::Manifest(error.to_string()))?;
    Ok(Response {
        status: 200,
        content_type: "application/json".to_string(),
        body,
    })
}

pub(crate) fn auth_session_response(root: &Path, body: &[u8]) -> PrayResult<Response> {
    let request: AuthSessionRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth session",
            message: error.to_string(),
        })?;
    let _ = (root, request);
    Ok(protected_auth_response())
}

pub(crate) fn auth_passkey_enroll_response(
    root: &Path,
    authorization: Option<&str>,
    body: &[u8],
) -> PrayResult<Response> {
    if !read_registry_trust_settings(root)?.passkeys_enabled {
        return Ok(disabled_auth_response());
    }
    let request: AuthPasskeyEnrollmentRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth passkey enrollment",
            message: error.to_string(),
        })?;
    let Some(session) = session_from_authorization(root, authorization)? else {
        return Ok(protected_auth_response());
    };
    if session.email != request.email {
        return Ok(protected_auth_response());
    }
    let store = RegistryAuthStore::open(root)?;
    let response: AuthPasskeyEnrollmentResponse = store.enroll_passkey(
        &request.email,
        &request.credential_id,
        &request.public_key,
        request.label.as_deref(),
    )?;
    json_ok(response)
}

pub(crate) fn auth_passkey_challenge_response(root: &Path, body: &[u8]) -> PrayResult<Response> {
    if !read_registry_trust_settings(root)?.passkeys_enabled {
        return Ok(disabled_auth_response());
    }
    let request: AuthPasskeyChallengeRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth passkey challenge",
            message: error.to_string(),
        })?;
    let store = RegistryAuthStore::open(root)?;
    let response: AuthPasskeyChallengeResponse =
        store.request_passkey_challenge(&request.credential_id)?;
    json_ok(response)
}

pub(crate) fn auth_passkey_login_response(root: &Path, body: &[u8]) -> PrayResult<Response> {
    if !read_registry_trust_settings(root)?.passkeys_enabled {
        return Ok(disabled_auth_response());
    }
    let request: AuthPasskeyLoginRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth passkey login",
            message: error.to_string(),
        })?;
    let store = RegistryAuthStore::open(root)?;
    let response: AuthPasskeyLoginResponse = store.respond_passkey_challenge(
        &request.credential_id,
        &request.challenge_id,
        &request.signature,
    )?;
    json_ok(response)
}

pub(crate) fn auth_ssh_key_challenge_response(root: &Path, body: &[u8]) -> PrayResult<Response> {
    if !read_registry_trust_settings(root)?.ssh_keys_enabled {
        return Ok(disabled_auth_response());
    }
    let request: AuthSshKeyChallengeRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth ssh key challenge",
            message: error.to_string(),
        })?;
    let store = RegistryAuthStore::open(root)?;
    let response: AuthSshKeyChallengeResponse =
        store.request_ssh_key_challenge(&request.public_key)?;
    json_ok(response)
}

pub(crate) fn auth_ssh_key_enroll_response(
    root: &Path,
    authorization: Option<&str>,
    body: &[u8],
) -> PrayResult<Response> {
    if !read_registry_trust_settings(root)?.ssh_keys_enabled {
        return Ok(disabled_auth_response());
    }
    let request: AuthSshKeyEnrollmentRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth ssh key enrollment",
            message: error.to_string(),
        })?;
    let Some(session) = session_from_authorization(root, authorization)? else {
        return Ok(protected_auth_response());
    };
    if session.email != request.email {
        return Ok(protected_auth_response());
    }
    let store = RegistryAuthStore::open(root)?;
    let response: AuthSshKeyEnrollmentResponse = store.enroll_ssh_key(
        &request.email,
        &request.public_key,
        request.label.as_deref(),
    )?;
    json_ok(response)
}

pub(crate) fn auth_ssh_key_login_response(root: &Path, body: &[u8]) -> PrayResult<Response> {
    if !read_registry_trust_settings(root)?.ssh_keys_enabled {
        return Ok(disabled_auth_response());
    }
    let request: AuthSshKeyLoginRequest =
        serde_json::from_slice(body).map_err(|error| PrayError::Parse {
            kind: "auth ssh key login",
            message: error.to_string(),
        })?;
    let store = RegistryAuthStore::open(root)?;
    let response: AuthSshKeyLoginResponse = store.respond_ssh_key_challenge(
        &request.public_key,
        &request.challenge_id,
        &request.signature,
    )?;
    json_ok(response)
}

fn session_from_authorization(
    root: &Path,
    authorization: Option<&str>,
) -> PrayResult<Option<AuthSessionResponse>> {
    let Some(token) = bearer_token_from_authorization(authorization) else {
        return Ok(None);
    };
    RegistryAuthStore::open(root)?.resolve_session(&token)
}

fn json_ok(value: impl serde::Serialize) -> PrayResult<Response> {
    let body =
        serde_json::to_vec(&value).map_err(|error| PrayError::Manifest(error.to_string()))?;
    Ok(Response {
        status: 200,
        content_type: "application/json".to_string(),
        body,
    })
}

fn protected_auth_response() -> Response {
    response_with_status(
        403,
        "text/plain",
        b"authentication proof is required for this operation".to_vec(),
    )
}

fn disabled_auth_response() -> Response {
    response_with_status(
        403,
        "text/plain",
        b"this authentication method is disabled".to_vec(),
    )
}

#[cfg(test)]
#[path = "server_auth_tests.rs"]
mod server_auth_handler_tests;