mycelium-api 8.3.1-rc.1

Provide API ports to the mycelium project.
use crate::rest::openid::shared::{
    get_authorization_providers, AuthorizationProvider,
};

use actix_web::{get, web, HttpResponse, Responder};
use awc::Client;
use myc_config::optional_config::OptionalConfig;
use myc_core::models::AccountLifeCycle;
use myc_http_tools::{
    models::auth_config::AuthConfig, settings::DEFAULT_CONNECTION_STRING_KEY,
};
use serde::{Deserialize, Serialize};
use utoipa::{ToResponse, ToSchema};

// ? ---------------------------------------------------------------------------
// ? Configure application
// ? ---------------------------------------------------------------------------

pub fn configure(config: &mut web::ServiceConfig) {
    config
        .service(well_known_oauth_authorization_server)
        .service(well_known_protected_resource);
}

// ? ---------------------------------------------------------------------------
// ? Define API paths
// ? ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize, Serialize, ToResponse, ToSchema)]
#[serde(rename_all = "snake_case")]
struct ProtectedResourceAuthServer {
    issuer: String,
    audience: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, ToResponse, ToSchema)]
#[serde(rename_all = "snake_case")]
struct ProtectedResource {
    resource: String,
    authorization_servers: Vec<ProtectedResourceAuthServer>,
    scopes_supported: Vec<String>,
    bearer_methods_supported: Vec<String>,
    resource_documentation: String,
}

/// Provide the well known openid configuration endpoint.
///
/// This endpoint is used to get the well known openid configuration from the
/// auth0 server.
///
#[utoipa::path(
    get,
    operation_id = "get_well_known_oauth_authorization_server",
    responses(
        (
            status = 200,
            description = "Well known oauth authorization server.",
            body = AuthorizationProvider,
        ),
    ),
)]
#[get("/.well-known/oauth-authorization-server")]
pub async fn well_known_oauth_authorization_server(
    auth_config: web::Data<AuthConfig>,
    client: web::Data<Client>,
) -> impl Responder {
    let auth_config = auth_config.get_ref();

    let external_config =
        if let OptionalConfig::Enabled(config) = &auth_config.external {
            config
        } else {
            return HttpResponse::NotFound()
                .body("External providers are not configured");
        };

    let eligible_providers = external_config
        .iter()
        .filter(|provider| provider.discovery_url.is_some())
        .map(|provider| provider.clone())
        .collect::<Vec<_>>();

    let authorization_providers = match get_authorization_providers(
        auth_config,
        Some(eligible_providers),
    )
    .await
    {
        Ok(providers) => providers,
        Err(error) => {
            return error;
        }
    };

    if authorization_providers.is_empty() {
        return HttpResponse::NotFound()
            .body("No authorization providers are configured");
    }

    let provider = authorization_providers.iter().next().unwrap();

    match client.get(provider.discovery_url.clone()).send().await {
        Err(err) => {
            tracing::error!(
                "Error fetching well known oauth authorization server from provider {}: {err}",
                provider.issuer
            );

            HttpResponse::InternalServerError().finish()
        }
        Ok(mut res) => {
            let body = res.body().await.unwrap_or_else(|_| web::Bytes::new());

            HttpResponse::build(res.status())
                .content_type("application/json")
                .body(body)
        }
    }
}

/// Provide the well known auth protected resources endpoint
///
/// This endpoint is used to get the well known auth protected resources from
/// the auth0 server.
///
/// Example:
///
/// ```json
/// {
///     "resource": "https://api.mycelium.example.com",
///     "authorization_servers": [
///         {
///             "issuer": "https://auth0.example.com",
///             "audience": "https://auth0.example.com/api/v2/"
///         },
///         {
///             "issuer": "https://accounts.google.com",
///             "audience": "YOUR_CLIENT_ID"
///         }
///     ],
///     "scopes_supported": ["read", "write"],
///     "bearer_methods_supported": ["header", "x-mycelium-connection-string"],
///     "resource_documentation": "https://lepistabioinformatics.github.io/mycelium-docs/"
/// }
/// ```
///
#[utoipa::path(
    get,
    operation_id = "get_well_known_oauth_protected_resource",
    responses(
        (
            status = 200,
            description = "Well known oauth protected resource.",
            body = AuthorizationProvider,
        ),
    ),
)]
#[get("/.well-known/oauth-protected-resource")]
pub async fn well_known_protected_resource(
    auth_config: web::Data<AuthConfig>,
    account_life_cycle: web::Data<AccountLifeCycle>,
) -> impl Responder {
    let auth_config = auth_config.get_ref();

    let authorization_servers =
        match get_authorization_providers(auth_config, None).await {
            Ok(providers) => providers
                .iter()
                .map(|p| ProtectedResourceAuthServer {
                    issuer: p.issuer.clone(),
                    audience: p.audience.clone(),
                })
                .collect::<Vec<_>>(),
            Err(error) => {
                return error;
            }
        };

    let resource = if let Some(domain_url) =
        account_life_cycle.domain_url.clone()
    {
        domain_url.async_get_or_error().await.unwrap()
    } else {
        return HttpResponse::NotFound().body("Domain URL is not configured");
    };

    let protected_resource = ProtectedResource {
        resource,
        authorization_servers,
        scopes_supported: vec![],
        bearer_methods_supported: vec![
            "header".to_string(),
            DEFAULT_CONNECTION_STRING_KEY.to_string(),
        ],
        resource_documentation:
            "https://lepistabioinformatics.github.io/mycelium-docs/".to_string(),
    };

    HttpResponse::Ok().json(protected_resource)
}