Skip to main content

platform_system_plane/
lib.rs

1//! Capability-neutral System Plane Core routing, registration, and negotiation.
2
3mod enrollment;
4mod module_operations;
5
6pub use enrollment::*;
7pub use module_operations::*;
8
9use axum::{
10    Extension, Json,
11    extract::FromRequestParts,
12    http::{HeaderMap, HeaderValue, StatusCode, header},
13    response::{IntoResponse, Response},
14};
15use lenso_service::{
16    AuthenticatedTransportBinding, WorkloadIdentityProvider, WorkloadIdentityVerification,
17    system_plane::{
18        CORE_PROTOCOL, CapabilityAdvertisement, CoreDocument, CoreIssue, validate_core_document,
19    },
20};
21use serde::Serialize;
22use std::{
23    collections::{BTreeSet, HashSet},
24    fmt,
25    sync::Arc,
26    time::{SystemTime, UNIX_EPOCH},
27};
28use utoipa::ToSchema;
29use utoipa_axum::{router::OpenApiRouter, routes};
30
31#[derive(Debug, Clone)]
32pub struct SystemPlaneRegistry {
33    document: Arc<CoreDocument>,
34}
35
36impl SystemPlaneRegistry {
37    pub fn new(document: CoreDocument) -> Result<Self, Vec<CoreIssue>> {
38        let issues = validate_core_document(&document);
39        if issues.is_empty() {
40            Ok(Self {
41                document: Arc::new(document),
42            })
43        } else {
44            Err(issues)
45        }
46    }
47
48    #[must_use]
49    pub fn document(&self) -> &CoreDocument {
50        &self.document
51    }
52
53    #[must_use]
54    pub fn negotiate(&self, requirements: &[CapabilityRequirement]) -> CapabilityNegotiation {
55        negotiate_capabilities(self.document(), requirements)
56    }
57}
58
59#[derive(Debug, Clone)]
60pub struct SystemPlaneRegistryBuilder {
61    document: CoreDocument,
62}
63
64impl SystemPlaneRegistryBuilder {
65    #[must_use]
66    pub fn new(
67        service_id: impl Into<String>,
68        service_principal: impl Into<String>,
69        service_revision: impl Into<String>,
70    ) -> Self {
71        Self {
72            document: CoreDocument {
73                protocol: CORE_PROTOCOL.to_owned(),
74                service_id: service_id.into(),
75                service_principal: service_principal.into(),
76                service_revision: service_revision.into(),
77                capabilities: Vec::new(),
78            },
79        }
80    }
81
82    #[must_use]
83    pub fn register(mut self, capability: CapabilityAdvertisement) -> Self {
84        self.document.capabilities.push(capability);
85        self
86    }
87
88    pub fn build(mut self) -> Result<SystemPlaneRegistry, Vec<CoreIssue>> {
89        self.document
90            .capabilities
91            .sort_by(|left, right| left.contract_id.cmp(&right.contract_id));
92        SystemPlaneRegistry::new(self.document)
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct CapabilityRequirement {
98    pub capability_id: String,
99    pub supported_major_versions: BTreeSet<u32>,
100    pub required_feature_ids: BTreeSet<String>,
101    pub accepted_schema_digests: BTreeSet<String>,
102}
103
104impl CapabilityRequirement {
105    #[must_use]
106    pub fn new(
107        capability_id: impl Into<String>,
108        supported_major_versions: impl IntoIterator<Item = u32>,
109    ) -> Self {
110        Self {
111            capability_id: capability_id.into(),
112            supported_major_versions: supported_major_versions.into_iter().collect(),
113            required_feature_ids: BTreeSet::new(),
114            accepted_schema_digests: BTreeSet::new(),
115        }
116    }
117
118    #[must_use]
119    pub fn requiring_features(
120        mut self,
121        feature_ids: impl IntoIterator<Item = impl Into<String>>,
122    ) -> Self {
123        self.required_feature_ids = feature_ids.into_iter().map(Into::into).collect();
124        self
125    }
126
127    #[must_use]
128    pub fn accepting_schema_digests(
129        mut self,
130        digests: impl IntoIterator<Item = impl Into<String>>,
131    ) -> Self {
132        self.accepted_schema_digests = digests.into_iter().map(Into::into).collect();
133        self
134    }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
138#[serde(rename_all = "snake_case")]
139pub enum CapabilityNegotiationIssueCode {
140    InvalidRequirement,
141    DuplicateRequirement,
142    MissingCapability,
143    UnsupportedMajorVersion,
144    MissingRequiredFeature,
145    SchemaDigestMismatch,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct CapabilityNegotiationIssue {
151    pub code: CapabilityNegotiationIssueCode,
152    pub capability_id: String,
153    pub message: String,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct NegotiatedCapability {
158    pub capability_id: String,
159    pub advertisement: CapabilityAdvertisement,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct CapabilityNegotiation {
164    pub accepted: Vec<NegotiatedCapability>,
165    pub issues: Vec<CapabilityNegotiationIssue>,
166}
167
168impl CapabilityNegotiation {
169    #[must_use]
170    pub const fn is_compatible(&self) -> bool {
171        self.issues.is_empty()
172    }
173}
174
175#[must_use]
176pub fn negotiate_capabilities(
177    document: &CoreDocument,
178    requirements: &[CapabilityRequirement],
179) -> CapabilityNegotiation {
180    let mut accepted = Vec::new();
181    let mut issues = Vec::new();
182    let mut seen = HashSet::new();
183
184    for requirement in requirements {
185        if !valid_capability_id(&requirement.capability_id)
186            || requirement.supported_major_versions.is_empty()
187            || requirement.supported_major_versions.contains(&0)
188        {
189            negotiation_issue(
190                &mut issues,
191                CapabilityNegotiationIssueCode::InvalidRequirement,
192                requirement,
193                "Capability requirements need a canonical identifier and at least one positive major version",
194            );
195            continue;
196        }
197        if !seen.insert(requirement.capability_id.as_str()) {
198            negotiation_issue(
199                &mut issues,
200                CapabilityNegotiationIssueCode::DuplicateRequirement,
201                requirement,
202                "Each capability may be negotiated once",
203            );
204            continue;
205        }
206
207        let prefix = format!("lenso.system-plane.{}.v", requirement.capability_id);
208        let candidates = document
209            .capabilities
210            .iter()
211            .filter(|capability| {
212                capability.contract_id.starts_with(&prefix)
213                    && capability.contract_id == format!("{}{}", prefix, capability.major_version)
214            })
215            .collect::<Vec<_>>();
216        if candidates.is_empty() {
217            negotiation_issue(
218                &mut issues,
219                CapabilityNegotiationIssueCode::MissingCapability,
220                requirement,
221                "The managed Service does not advertise this capability",
222            );
223            continue;
224        }
225        let candidate = candidates
226            .into_iter()
227            .filter(|capability| {
228                requirement
229                    .supported_major_versions
230                    .contains(&capability.major_version)
231            })
232            .max_by_key(|capability| capability.major_version);
233        let Some(candidate) = candidate else {
234            negotiation_issue(
235                &mut issues,
236                CapabilityNegotiationIssueCode::UnsupportedMajorVersion,
237                requirement,
238                "The managed Service and consumer share no supported major version",
239            );
240            continue;
241        };
242        if !requirement
243            .required_feature_ids
244            .is_subset(&candidate.feature_ids)
245        {
246            negotiation_issue(
247                &mut issues,
248                CapabilityNegotiationIssueCode::MissingRequiredFeature,
249                requirement,
250                "The advertised contract is missing a required feature identifier",
251            );
252            continue;
253        }
254        if !requirement.accepted_schema_digests.is_empty()
255            && !requirement
256                .accepted_schema_digests
257                .contains(&candidate.schema_digest)
258        {
259            negotiation_issue(
260                &mut issues,
261                CapabilityNegotiationIssueCode::SchemaDigestMismatch,
262                requirement,
263                "The advertised schema digest is not accepted by the consumer",
264            );
265            continue;
266        }
267        accepted.push(NegotiatedCapability {
268            capability_id: requirement.capability_id.clone(),
269            advertisement: candidate.clone(),
270        });
271    }
272
273    accepted.sort_by(|left, right| left.capability_id.cmp(&right.capability_id));
274    CapabilityNegotiation { accepted, issues }
275}
276
277fn negotiation_issue(
278    issues: &mut Vec<CapabilityNegotiationIssue>,
279    code: CapabilityNegotiationIssueCode,
280    requirement: &CapabilityRequirement,
281    message: &str,
282) {
283    issues.push(CapabilityNegotiationIssue {
284        code,
285        capability_id: requirement.capability_id.clone(),
286        message: message.to_owned(),
287    });
288}
289
290fn valid_capability_id(value: &str) -> bool {
291    !value.is_empty()
292        && value.split('.').all(|segment| {
293            !segment.is_empty()
294                && !segment.starts_with('-')
295                && !segment.ends_with('-')
296                && !segment.contains("--")
297                && segment
298                    .bytes()
299                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
300        })
301}
302
303#[derive(Clone)]
304pub struct SystemPlaneAccess {
305    provider: Arc<dyn WorkloadIdentityProvider>,
306    audience: String,
307    enrollment_authorizer: Arc<dyn EnrollmentAuthorizer>,
308}
309
310impl fmt::Debug for SystemPlaneAccess {
311    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312        formatter
313            .debug_struct("SystemPlaneAccess")
314            .field("provider", &self.provider)
315            .field("audience", &self.audience)
316            .field("enrollment_authorizer", &self.enrollment_authorizer)
317            .finish()
318    }
319}
320
321impl SystemPlaneAccess {
322    #[must_use]
323    pub fn new(
324        provider: Arc<dyn WorkloadIdentityProvider>,
325        audience: impl Into<String>,
326        enrollment_authorizer: Arc<dyn EnrollmentAuthorizer>,
327    ) -> Self {
328        Self {
329            provider,
330            audience: audience.into(),
331            enrollment_authorizer,
332        }
333    }
334}
335
336#[derive(Debug, Clone)]
337pub struct SystemPlaneRuntime {
338    pub registry: Arc<SystemPlaneRegistry>,
339    pub access: Arc<SystemPlaneAccess>,
340    pub module_operations: Option<Arc<ModuleOperationsProvider>>,
341}
342
343impl SystemPlaneRuntime {
344    #[must_use]
345    pub fn new(registry: SystemPlaneRegistry, access: SystemPlaneAccess) -> Self {
346        Self {
347            registry: Arc::new(registry),
348            access: Arc::new(access),
349            module_operations: None,
350        }
351    }
352
353    #[must_use]
354    pub fn with_module_operations(mut self, provider: ModuleOperationsProvider) -> Self {
355        self.module_operations = Some(Arc::new(provider));
356        self
357    }
358}
359
360#[derive(Debug, Serialize, ToSchema)]
361#[serde(rename_all = "camelCase")]
362pub struct SystemPlaneErrorBody {
363    pub code: &'static str,
364    pub message: String,
365    pub next_actions: Vec<&'static str>,
366}
367
368#[derive(Debug)]
369pub struct SystemPlaneRejection {
370    status: StatusCode,
371    code: &'static str,
372    message: String,
373    next_action: &'static str,
374}
375
376impl SystemPlaneRejection {
377    #[must_use]
378    pub fn new(
379        status: StatusCode,
380        code: &'static str,
381        message: impl Into<String>,
382        next_action: &'static str,
383    ) -> Self {
384        Self {
385            status,
386            code,
387            message: message.into(),
388            next_action,
389        }
390    }
391
392    #[must_use]
393    pub fn unavailable(
394        code: &'static str,
395        message: impl Into<String>,
396        next_action: &'static str,
397    ) -> Self {
398        Self {
399            status: StatusCode::SERVICE_UNAVAILABLE,
400            code,
401            message: message.into(),
402            next_action,
403        }
404    }
405
406    #[must_use]
407    pub const fn code(&self) -> &'static str {
408        self.code
409    }
410}
411
412impl IntoResponse for SystemPlaneRejection {
413    fn into_response(self) -> Response {
414        let body = SystemPlaneErrorBody {
415            code: self.code,
416            message: self.message,
417            next_actions: vec![self.next_action],
418        };
419        let mut response = (self.status, Json(body)).into_response();
420        response.headers_mut().insert(
421            header::CONTENT_TYPE,
422            HeaderValue::from_static("application/problem+json"),
423        );
424        response
425            .headers_mut()
426            .insert("x-lenso-error-code", HeaderValue::from_static(self.code));
427        response
428    }
429}
430
431#[derive(Debug, Clone)]
432pub struct AuthorizedSystemPlaneCaller {
433    pub runtime: Arc<SystemPlaneRuntime>,
434    pub service_principal: String,
435    pub enrollment: EnrollmentAuthorization,
436}
437
438impl AuthorizedSystemPlaneCaller {
439    pub fn require_capability(
440        &self,
441        contract_id: &str,
442        schema_digest: &str,
443        required_feature_ids: impl IntoIterator<Item = impl AsRef<str>>,
444    ) -> Result<(), SystemPlaneRejection> {
445        if self.enrollment.system_id == "system-sandbox" {
446            return Ok(());
447        }
448        let required_feature_ids = required_feature_ids
449            .into_iter()
450            .map(|feature| feature.as_ref().to_owned())
451            .collect::<BTreeSet<_>>();
452        let granted = self.enrollment.capabilities.iter().any(|capability| {
453            capability.contract_id == contract_id
454                && capability.schema_digest == schema_digest
455                && required_feature_ids.is_subset(&capability.feature_ids)
456        });
457        if granted {
458            Ok(())
459        } else {
460            Err(SystemPlaneRejection {
461                status: StatusCode::FORBIDDEN,
462                code: "system_plane_capability_not_granted",
463                message: "Active Enrollment Grant does not authorize the requested capability"
464                    .to_owned(),
465                next_action: "review_service_enrollment_grant",
466            })
467        }
468    }
469}
470
471impl<S> FromRequestParts<S> for AuthorizedSystemPlaneCaller
472where
473    S: Send + Sync,
474{
475    type Rejection = SystemPlaneRejection;
476
477    async fn from_request_parts(
478        parts: &mut axum::http::request::Parts,
479        _state: &S,
480    ) -> Result<Self, Self::Rejection> {
481        let runtime = parts
482            .extensions
483            .get::<Option<Arc<SystemPlaneRuntime>>>()
484            .and_then(Clone::clone)
485            .ok_or_else(|| SystemPlaneRejection {
486                status: StatusCode::SERVICE_UNAVAILABLE,
487                code: "system_plane_unavailable",
488                message: "System Plane access is not configured for this Service".to_owned(),
489                next_action: "configure_system_plane",
490            })?;
491        let token = bearer_token(&parts.headers)?;
492        let binding = parts
493            .extensions
494            .get::<AuthenticatedTransportBinding>()
495            .ok_or_else(|| SystemPlaneRejection {
496                status: StatusCode::UNAUTHORIZED,
497                code: "system_plane_transport_binding_required",
498                message: "System Plane access requires an authenticated transport binding"
499                    .to_owned(),
500                next_action: "use_authenticated_transport",
501            })?;
502        let principal = runtime
503            .access
504            .provider
505            .verify(
506                token,
507                &WorkloadIdentityVerification::new(
508                    &runtime.access.audience,
509                    &binding.0,
510                    now_unix_ms(),
511                ),
512            )
513            .map_err(|error| SystemPlaneRejection {
514                status: StatusCode::UNAUTHORIZED,
515                code: "system_plane_workload_identity_rejected",
516                message: error.message,
517                next_action: "refresh_workload_identity",
518            })?;
519        let enrollment = runtime
520            .access
521            .enrollment_authorizer
522            .authorize(
523                &runtime.registry.document().service_id,
524                &principal.service_principal,
525                now_unix_ms(),
526            )
527            .await
528            .map_err(enrollment_rejection)?;
529        Ok(Self {
530            runtime,
531            service_principal: principal.service_principal,
532            enrollment,
533        })
534    }
535}
536
537/// Builds the mandatory Core discovery route. An absent runtime fails closed.
538#[must_use]
539pub fn router<S>(runtime: Option<Arc<SystemPlaneRuntime>>) -> OpenApiRouter<S>
540where
541    S: Clone + Send + Sync + 'static,
542{
543    OpenApiRouter::new()
544        .routes(routes!(discover_core))
545        .layer(Extension(runtime))
546}
547
548#[utoipa::path(
549    get,
550    path = "/system-plane/v1",
551    responses(
552        (status = 200, description = "Authenticated System Plane Core document", body = CoreDocument),
553        (status = 401, description = "Workload Identity or transport binding was not accepted", body = SystemPlaneErrorBody, content_type = "application/problem+json"),
554        (status = 403, description = "Caller is not the enrolled Console Service Principal", body = SystemPlaneErrorBody, content_type = "application/problem+json"),
555        (status = 503, description = "System Plane access is not configured", body = SystemPlaneErrorBody, content_type = "application/problem+json")
556    ),
557    security(("bearer_auth" = [])),
558    tag = "system-plane"
559)]
560async fn discover_core(caller: AuthorizedSystemPlaneCaller) -> Json<CoreDocument> {
561    Json(caller.runtime.registry.document().clone())
562}
563
564fn bearer_token(headers: &HeaderMap) -> Result<&str, SystemPlaneRejection> {
565    headers
566        .get(header::AUTHORIZATION)
567        .and_then(|value| value.to_str().ok())
568        .and_then(|value| value.strip_prefix("Bearer "))
569        .filter(|value| !value.is_empty())
570        .ok_or_else(|| SystemPlaneRejection {
571            status: StatusCode::UNAUTHORIZED,
572            code: "system_plane_workload_identity_required",
573            message: "System Plane access requires a Workload Identity Bearer credential"
574                .to_owned(),
575            next_action: "provide_workload_identity",
576        })
577}
578
579fn enrollment_rejection(error: EnrollmentError) -> SystemPlaneRejection {
580    let (status, code, next_action) = match error.code {
581        EnrollmentErrorCode::StoreUnavailable => (
582            StatusCode::SERVICE_UNAVAILABLE,
583            "system_plane_enrollment_unavailable",
584            "restore_enrollment_store",
585        ),
586        EnrollmentErrorCode::Expired => (
587            StatusCode::FORBIDDEN,
588            "system_plane_enrollment_expired",
589            "renew_service_enrollment",
590        ),
591        EnrollmentErrorCode::Revoked => (
592            StatusCode::FORBIDDEN,
593            "system_plane_enrollment_revoked",
594            "complete_service_enrollment",
595        ),
596        EnrollmentErrorCode::PrincipalMismatch => (
597            StatusCode::FORBIDDEN,
598            "system_plane_console_not_enrolled",
599            "complete_service_enrollment",
600        ),
601        EnrollmentErrorCode::NotEnrolled
602        | EnrollmentErrorCode::InvalidGrant
603        | EnrollmentErrorCode::InvalidDecision
604        | EnrollmentErrorCode::SignatureRejected
605        | EnrollmentErrorCode::NonceReused
606        | EnrollmentErrorCode::AlreadyEnrolled
607        | EnrollmentErrorCode::StaleAuthorizationEpoch => (
608            StatusCode::FORBIDDEN,
609            "system_plane_enrollment_required",
610            "complete_service_enrollment",
611        ),
612    };
613    SystemPlaneRejection {
614        status,
615        code,
616        message: error.message,
617        next_action,
618    }
619}
620
621fn now_unix_ms() -> u64 {
622    SystemTime::now()
623        .duration_since(UNIX_EPOCH)
624        .map_or(0, |duration| {
625            u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
626        })
627}