use std::sync::Arc;
use reqwest::Method;
use secrecy::{ExposeSecret, SecretString};
use serde::Serialize;
use serde::de::DeserializeOwned;
use uuid::Uuid;
use wyrd_spec::DataTenantId;
use wyrd_spec::auth::{
ConfigurePlatformOidcRequest, CreateTenantRequest, CreateTenantResponse,
CredentialListResponse, IssuePlatformCredentialRequest, IssuedCredential,
PlatformOidcConnectionView, PlatformPrincipalListResponse, PrincipalId, ProvisionedTenant,
ProvisionedTenantAdmin, RecoverTenantAdminRequest, RegisterPlatformAdminRequest,
RegisterPlatformAdminResponse, SecretBearer, SetPlatformPrincipalStatusRequest,
SetTenantStatusRequest, TenantListResponse,
};
use wyrd_spec::error::WyrdError;
use crate::auth::{AuthMiddleware, TokenExchange};
use crate::client::WyrdClient;
use crate::config::ClientConfig;
use crate::transport::config::HttpConfig;
use crate::transport::credential::ResolvedCredential;
use crate::transport::http::HttpTransport;
use std::fmt::{Debug, Formatter, Result as FmtResult};
#[derive(Clone)]
pub struct PlatformSession(SecretString);
impl Debug for PlatformSession {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_tuple("PlatformSession")
.field(&"[REDACTED]")
.finish()
}
}
#[derive(Clone, Debug)]
pub struct Platform {
client: Arc<WyrdClient>,
}
impl Platform {
pub async fn connect() -> Result<Self, WyrdError> {
Self::connect_with(&WyrdClient::from_global().map_err(WyrdError::from)?).await
}
pub async fn connect_with(client: &WyrdClient) -> Result<Self, WyrdError> {
let auth = &client.auth;
let ResolvedCredential::BearerToken(credential) = auth.credential() else {
return Err(WyrdError::Unauthenticated {
message: "a platform client needs a platform credential".to_owned(),
details: serde_json::json!({}),
});
};
let config = ClientConfig {
http: HttpConfig {
base_url: client.server_url().trim_end_matches('/').to_owned(),
..HttpConfig::default()
},
..ClientConfig::default()
};
let exchange = TokenExchange::new(&config.http.base_url, config.http.timeout_ms)
.map_err(WyrdError::from)?;
let response = exchange
.platform_session(&SecretBearer::new(credential.expose_secret().to_owned()))
.await
.map_err(crate::auth::AuthError::into_wyrd)?;
let session = PlatformSession(SecretString::from(
response.access_token.expose().to_owned(),
));
Self::with_session(config, session)
}
fn with_session(config: ClientConfig, session: PlatformSession) -> Result<Self, WyrdError> {
let auth = AuthMiddleware::new(&config, ResolvedCredential::BearerToken(session.0.clone()))
.map_err(WyrdError::from)?;
let http = HttpTransport::new(&config.http, Arc::clone(&auth)).map_err(WyrdError::from)?;
Ok(Self {
client: Arc::new(WyrdClient {
auth,
http,
grpc_config: config.grpc,
}),
})
}
pub async fn create_tenant(
&self,
request: &CreateTenantRequest,
) -> Result<CreateTenantResponse, WyrdError> {
self.call(Method::POST, "/platform/tenants", Some(request))
.await
}
pub async fn list_tenants(&self) -> Result<TenantListResponse, WyrdError> {
self.call::<(), _>(Method::GET, "/platform/tenants", None)
.await
}
pub async fn tenant(&self, tenant_id: DataTenantId) -> Result<ProvisionedTenant, WyrdError> {
self.call::<(), _>(Method::GET, &format!("/platform/tenants/{tenant_id}"), None)
.await
}
pub async fn set_tenant_status(
&self,
tenant_id: DataTenantId,
status: &str,
) -> Result<(), WyrdError> {
let request = SetTenantStatusRequest {
status: status.to_owned(),
};
self.call_no_content(
Method::PUT,
&format!("/platform/tenants/{tenant_id}/status"),
Some(&request),
)
.await
}
pub async fn recover_tenant_admin(
&self,
tenant_id: DataTenantId,
) -> Result<ProvisionedTenantAdmin, WyrdError> {
self.call(
Method::POST,
"/platform/tenants/admin/credentials",
Some(&RecoverTenantAdminRequest { tenant_id }),
)
.await
}
pub async fn configure_oidc(
&self,
request: &ConfigurePlatformOidcRequest,
) -> Result<PlatformOidcConnectionView, WyrdError> {
self.call(Method::PUT, "/platform/oidc/connection", Some(request))
.await
}
pub async fn oidc_connection(&self) -> Result<PlatformOidcConnectionView, WyrdError> {
self.call::<(), _>(Method::GET, "/platform/oidc/connection", None)
.await
}
pub async fn register_admin(
&self,
request: &RegisterPlatformAdminRequest,
) -> Result<RegisterPlatformAdminResponse, WyrdError> {
self.call(Method::POST, "/platform/admins", Some(request))
.await
}
pub async fn list_admins(&self) -> Result<PlatformPrincipalListResponse, WyrdError> {
self.call::<(), _>(Method::GET, "/platform/admins", None)
.await
}
pub async fn set_admin_status(
&self,
principal_id: &PrincipalId,
status: &str,
) -> Result<(), WyrdError> {
let request = SetPlatformPrincipalStatusRequest {
status: status.to_owned(),
};
self.call_no_content(
Method::PUT,
&format!("/platform/admins/{principal_id}/status"),
Some(&request),
)
.await
}
pub async fn issue_credential(
&self,
principal_id: &PrincipalId,
expires_in_days: Option<u32>,
) -> Result<IssuedCredential, WyrdError> {
self.call(
Method::POST,
&format!("/platform/admins/{principal_id}/credentials"),
Some(&IssuePlatformCredentialRequest { expires_in_days }),
)
.await
}
pub async fn list_credentials(
&self,
principal_id: &PrincipalId,
) -> Result<CredentialListResponse, WyrdError> {
self.call::<(), _>(
Method::GET,
&format!("/platform/admins/{principal_id}/credentials"),
None,
)
.await
}
pub async fn revoke_credential(
&self,
principal_id: &PrincipalId,
credential_id: Uuid,
) -> Result<(), WyrdError> {
self.call_no_content::<()>(
Method::DELETE,
&format!("/platform/admins/{principal_id}/credentials/{credential_id}"),
None,
)
.await
}
async fn call<S, D>(&self, method: Method, path: &str, body: Option<&S>) -> Result<D, WyrdError>
where
S: Serialize,
D: DeserializeOwned,
{
self.client.http.request_json(method, path, body).await
}
async fn call_no_content<S>(
&self,
method: Method,
path: &str,
body: Option<&S>,
) -> Result<(), WyrdError>
where
S: Serialize,
{
self.client
.http
.request_json::<S, ()>(method, path, body)
.await
}
}