opencloudmesh 0.2.1

Implementation of the OpenCloudMesh protocol
Documentation
// SPDX-FileCopyrightText: 2026 Matthias Kraus <info@opengeomesh.org>
//
// SPDX-License-Identifier: LGPL-3.0-or-later

use http::StatusCode;

use ocm_types::{
    common::ShareType,
    discovery::{Criterium, Discovery},
    error::{Error, ValidationError},
    share::{
        NewShare, SendingServer, ShareCreationResponse,
    },
};
use serde::Serialize;

use crate::drivers::{
        protocols::MultiProtocol,
        shares::{ReceivedShareRepo, ShareRepoError},
        users::{UserRepo, UserRepoError},
    }   ;
/// Receive a share from a remote sender.
///
/// If the received share is acceptable the share is stored in the provided [ReceivedShareRepo].
/// Success or Errors are returned as OCM Types.
///
/// * `received_share_repo` acceptable received shares are stored in this share repo
/// * `user_repo` the provided [UserRepo] is used to check if the recipient exists on this server.
/// * `ocm_server_fqdn` the fqdn of the OCM Server is used to check if the fqdn of the recipient matches.
/// * `new_share` the received share
/// * `criteria` to check when validating the received `new_share`
/// * `supported_protocols` for accessing shares. Shares which are not provided via any of the
///   supported [Protocols](crate::drivers::protocols::Protocol) are rejected.
/// * `sending_server_discovery` Details of the SendingServer of this share. 
/// * `authenticated_sending_server` Sending Server authenticated based on the request http
///   signature. Authentication must be guaranteed by the caller!
// FIXME differentiate SendingServer and AuthenticatedSendingServer via types
pub async fn receive_share<S: ReceivedShareRepo, U: UserRepo>(
    // client: &impl HttpClient,
    received_share_repo: &S,
    user_repo: &U,
    ocm_server_fqdn: &str,
    new_share: NewShare,
    criteria: Vec<Criterium>,
    supported_protocols: &Vec<Box<dyn crate::drivers::protocols::Protocol>>,
    sending_server_discovery: &Discovery,
    authenticated_sending_server: Option<SendingServer>,
) -> Result<ShareCreationResponse, ReceiveShareError> {
    let sending_server = new_share.sending_server();

    // check if
    // TODO check if receiver supports share type

    if criteria.contains(&Criterium::HttpRequestSignatures) {
        match authenticated_sending_server {
            // the HTTP Signature is missing
            None => {
                Err(ReceiveShareError::MissingHttpSignature)?;
            }
            // the keypair used to generate the HTTP Signature doesn't match the one
            // trusted or discoverable from the FQDN part of the sender field in the
            // request body
            Some(authenticated_sending_server)
                if sending_server != authenticated_sending_server =>
            {
                dbg!(&sending_server, &authenticated_sending_server);
                Err(ReceiveShareError::InvalidHttpSignature)?;
            }
            Some(_) => (),
        }
    }

    // TODO the Sending Server is denylisted
    // TODO the Sending Server is not allowlisted
    // TODO the Sending Party is not trusted by the Receiving Party (e.g. no Invite was exchanged and/or the Sending Party's OCM Address does not appear in the Receiving Party's address book)
    // TODO the Receiving Server is unable to act as an API client for (any of) the protocol(s) listed for accessing the resource
    // TODO an initial check shows that the resource cannot successfully accessed through (any of) the protocol(s) listed
    let protocols = normalize_protocols(&new_share, supported_protocols, sending_server_discovery)?;

    // check if the fqdn of the recipient matches the fqdn of this server
    // TODO is this check necessary? or should this be omitted to enable webfinger aliases?
    if new_share.share_with.get_server_url() != ocm_server_fqdn {
        Err(ReceiveShareError::InvalidShareWith(format!(
            "Recipient FQDN '{}' must match FQDN of this OCM Server '{}'",
            new_share.share_with.get_server_url(),
            ocm_server_fqdn
        )))?
    };

    let recipient_user_id = new_share.share_with.get_identifier();

    let recipient = if new_share.share_type == ShareType::User {
        // check if recipient exists
        user_repo.get(recipient_user_id).await?
    } else {
        Err(ReceiveShareError::UnsupportedShare(
            "Currently only share type 'user' is supported!".to_string(),
        ))?
    };

    // TODO check if recipient may receive shares

    // store received share
    received_share_repo.insert(sending_server, (new_share, protocols)).await?;

    // TODO receiving party notification (inform recipient about received share, optional decision
    // to accept / reject share)

    Ok(ShareCreationResponse {
        recipient_display_name: Some(recipient.name),
        recipient_public_keys: vec![] // TODO a protocol implementation should be able to modify
        // this!
    })
}

fn normalize_protocols(
    new_share: &NewShare,
    supported_protocols: &Vec<Box<dyn crate::drivers::protocols::Protocol + 'static>>,
    sending_server_discovery: &Discovery,
) -> Result<MultiProtocol, ReceiveShareError> {
    let normalized_protocol_properties = supported_protocols
        .iter()
        .filter_map(|protocol| {
            // FIXME this silently drops errors, at least add logging
            protocol
                .resolve_client_properties(sending_server_discovery, new_share)
                .ok()
        })
        .fold(MultiProtocol::default(), |acc, p| {
            acc.merge(p)
        });
    if normalized_protocol_properties.is_empty() {
        Err(ReceiveShareError::UnsupportedShare(
            "Share is not accessible via any supported protocol.".to_string(),
        ))?
    };
    Ok(normalized_protocol_properties)
}

#[derive(Debug, Clone, Serialize)]
#[serde(into = "Error")]
/// Errors when receiving shares.
pub enum ReceiveShareError {
    InvalidShareWith(String),
    UserNotFound { user: String },
    UnsupportedShare(String),
    InvalidSender(String),
    MissingHttpSignature,
    InvalidHttpSignature,
}

impl From<ReceiveShareError> for Error {
    fn from(value: ReceiveShareError) -> Self {
        match value {
            ReceiveShareError::UserNotFound { user } => Error {
                message: format!("User {user} does not exist"),
                validation_errors: vec![],
            },
            ReceiveShareError::UnsupportedShare(message) => Error {
                message,
                validation_errors: vec![],
            },
            ReceiveShareError::InvalidShareWith(share_with) => Error {
                message: format!("INVALID_SHARE_WITH {share_with}"),
                validation_errors: vec![],
            },
            ReceiveShareError::InvalidSender(sender) => Error {
                message: format!("INVALID_SENDER {sender}"),
                validation_errors: vec![],
            },
            ReceiveShareError::MissingHttpSignature => Error {
                message: "MISSING_HTTP_SIGNATURE".to_string(),
                validation_errors: vec![],
            },
            ReceiveShareError::InvalidHttpSignature => Error {
                message: "INVALID_HTTP_SIGNATURE".to_string(),
                validation_errors: vec![ValidationError {
                    name: None,
                    message: Some(
                        "Sending Server from the http signature must match the FQDN of the sender"
                            .to_string(),
                    ),
                }],
            },
        }
    }
}

// TODO should this be moved to ocm-server-axum?
impl ReceiveShareError {
    pub fn status_code(&self) -> StatusCode {
        match self {
            ReceiveShareError::InvalidShareWith(_) => StatusCode::NOT_ACCEPTABLE,
            ReceiveShareError::UserNotFound { .. } => StatusCode::NOT_FOUND,
            ReceiveShareError::UnsupportedShare(_) => StatusCode::NOT_ACCEPTABLE,
            ReceiveShareError::InvalidSender(_) => StatusCode::NOT_ACCEPTABLE,
            ReceiveShareError::MissingHttpSignature => StatusCode::UNAUTHORIZED,
            ReceiveShareError::InvalidHttpSignature => StatusCode::UNAUTHORIZED,
        }
    }
}

impl From<UserRepoError> for ReceiveShareError {
    fn from(value: UserRepoError) -> Self {
        match value {
            UserRepoError::RepoAccessFailed => todo!(),
            UserRepoError::NotFound(user) => ReceiveShareError::UserNotFound { user },
        }
    }
}

impl From<ShareRepoError> for ReceiveShareError {
    fn from(value: ShareRepoError) -> Self {
        match value {
            ShareRepoError::NotFound => panic!("This should not happen"),
        }
    }
}