diff --git a/TASKS.md b/TASKS.md index 84ee48c..6e7811b 100644 --- a/TASKS.md +++ b/TASKS.md @@ -165,4 +165,5 @@ Progress: - Phase 1 / task `1.7`: workspace version bumped to `0.2.0`; release gate пройден (`just verify`, `npm run build`, `npm run e2e`), release commit и tag `v0.2.0` подготовлены в `crank-community` - Phase 2 dependency slice: `IdentityProvider` seam widened to cover `SSO/TOTP` with default `NotSupportedForProvider` methods and shared public DTOs for authorize/callback/two-factor flows; workspace version bumped to `0.3.0` for downstream enterprise consumption - Phase 2 dependency slice: `apps/admin-api` now exposes a reusable lib target (`src/lib.rs`), so downstream private repos can depend on `build_app`, `AppState`, `AdminServiceBuilder`, and auth/state modules without copying the Community admin app + - Phase 3 dependency slice: added public tenancy seam in `crank-core` — `TenantId`, `TenantResolutionContext`, `TenantController`, `TenancyError`, and `SingleTenantController` — so `cloud` can build hosted tenant routing without bypassing the shared extension model - backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено diff --git a/crates/crank-core/src/ext/mod.rs b/crates/crank-core/src/ext/mod.rs index 69d10c5..a655379 100644 --- a/crates/crank-core/src/ext/mod.rs +++ b/crates/crank-core/src/ext/mod.rs @@ -3,3 +3,4 @@ pub mod audit; pub mod auth; pub mod capability; pub mod protocol; +pub mod tenancy; diff --git a/crates/crank-core/src/ext/tenancy.rs b/crates/crank-core/src/ext/tenancy.rs new file mode 100644 index 0000000..70668ad --- /dev/null +++ b/crates/crank-core/src/ext/tenancy.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{TenantId, WorkspaceId}; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TenantResolutionContext { + pub workspace_id: Option, + pub workspace_slug: Option, + pub host: Option, +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum TenancyError { + #[error("tenant lookup is not supported in this edition")] + UnsupportedLookup, + #[error("internal tenancy failure: {0}")] + Internal(String), +} + +#[async_trait] +pub trait TenantController: Send + Sync { + fn resolve_tenant(&self, request: &TenantResolutionContext) -> Result; + + async fn list_workspaces_for_tenant( + &self, + tenant: &TenantId, + ) -> Result, TenancyError>; +} + +pub type SharedTenantController = Arc; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SingleTenantController { + tenant_id: TenantId, +} + +impl Default for SingleTenantController { + fn default() -> Self { + Self { + tenant_id: TenantId::new("default"), + } + } +} + +#[async_trait] +impl TenantController for SingleTenantController { + fn resolve_tenant( + &self, + _request: &TenantResolutionContext, + ) -> Result { + Ok(self.tenant_id.clone()) + } + + async fn list_workspaces_for_tenant( + &self, + _tenant: &TenantId, + ) -> Result, TenancyError> { + Ok(Vec::new()) + } +} + +#[cfg(test)] +mod tests { + use super::{SingleTenantController, TenantController, TenantResolutionContext}; + use crate::TenantId; + + #[tokio::test] + async fn single_tenant_controller_returns_default_tenant() { + let controller = SingleTenantController::default(); + + let tenant = controller + .resolve_tenant(&TenantResolutionContext::default()) + .unwrap(); + let workspaces = controller.list_workspaces_for_tenant(&tenant).await.unwrap(); + + assert_eq!(tenant, TenantId::new("default")); + assert!(workspaces.is_empty()); + } +} diff --git a/crates/crank-core/src/ids.rs b/crates/crank-core/src/ids.rs index 6942a56..b1d98b5 100644 --- a/crates/crank-core/src/ids.rs +++ b/crates/crank-core/src/ids.rs @@ -48,6 +48,7 @@ define_id!(ToolId); define_id!(SampleId); define_id!(AuthProfileId); define_id!(WorkspaceId); +define_id!(TenantId); define_id!(UserId); define_id!(UserSessionId); define_id!(AgentId); diff --git a/crates/crank-core/src/lib.rs b/crates/crank-core/src/lib.rs index e7b21c9..457aa35 100644 --- a/crates/crank-core/src/lib.rs +++ b/crates/crank-core/src/lib.rs @@ -52,9 +52,13 @@ pub use ext::protocol::{ AdapterRegistry, AdapterResponse, PreparedRequest, ProtocolAdapter, ProtocolAdapterError, ResponseCacheScope, RuntimeRequestContext, SharedProtocolAdapter, WindowExecutionResult, }; +pub use ext::tenancy::{ + SharedTenantController, SingleTenantController, TenantController, TenantResolutionContext, + TenancyError, +}; pub use ids::{ AgentId, AsyncJobId, AuditEventId, AuthProfileId, DescriptorId, InvitationId, InvocationLogId, - OperationId, PlatformApiKeyId, SampleId, SecretId, StreamSessionId, ToolId, UserId, + OperationId, PlatformApiKeyId, SampleId, SecretId, StreamSessionId, TenantId, ToolId, UserId, UserSessionId, WorkspaceId, }; pub use observability::{