Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions src/adapter/http/glance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};

use super::{append_pagination_parts, encode_param, extract_marker_from_url, paginated_list};
use crate::adapter::http::base::BaseHttpClient;
use crate::adapter::http::neutron_audit::GlanceAuditCtx;
use crate::adapter::http::scope_refilter::{RefilterScope, refilter_and_audit};
use crate::models::glance::Image;
use crate::port::auth::AuthProvider;
use crate::port::error::{ApiError, ApiResult};
Expand All @@ -13,6 +15,7 @@ use crate::port::types::*;

pub struct GlanceHttpAdapter {
base: Arc<BaseHttpClient>,
audit_ctx: Option<Arc<GlanceAuditCtx>>,
}

impl GlanceHttpAdapter {
Expand All @@ -24,11 +27,68 @@ impl GlanceHttpAdapter {
EndpointInterface::Public,
region,
)?),
audit_ctx: None,
})
}

pub fn from_base(base: Arc<BaseHttpClient>) -> Self {
Self { base }
Self {
base,
audit_ctx: None,
}
}

/// BL-P2-091: attach a `GlanceAuditCtx` so `list_images` runs
/// `refilter_by_scope` against the response and emits an
/// `AdapterFilterViolation` audit event per dropped row. Mirrors the
/// Neutron/Nova/Cinder pattern wired by `registry::new_http`.
pub fn with_audit(mut self, ctx: Arc<GlanceAuditCtx>) -> Self {
self.audit_ctx = Some(ctx);
self
}

/// Apply response-side scope refiltering to a `PaginatedResponse`.
/// No-op when `audit_ctx` is None (mock registries, integration tests
/// without audit). When attached, partitions via
/// [`refilter_and_audit`] which fans out one `AdapterFilterViolation`
/// event per dropped row before returning the kept items.
///
/// Glance v2 surfaces `Image.owner` rather than `tenant_id`; the
/// `ScopedItem for Image` impl bridges the two so this helper stays
/// identical to the Neutron/Nova/Cinder shape.
///
/// [`refilter_and_audit`]: crate::adapter::http::scope_refilter::refilter_and_audit
fn refilter_response<T>(
&self,
resp: PaginatedResponse<T>,
all_tenants: bool,
action_type: &str,
resource_kind: &str,
) -> PaginatedResponse<T>
where
T: crate::adapter::http::scope_refilter::ScopedItem,
{
let active = self
.audit_ctx
.as_ref()
.and_then(|ctx| ctx.scope_provider.current_project_id());
let scope = RefilterScope::from_parts(active.as_deref(), all_tenants);
// correlation_id=0 mirrors Neutron: list_* are not bound to a
// worker dispatch, and fingerprint resource_id disambiguates
// per-row events. Epoch propagation lands in a separate cycle.
let kept = refilter_and_audit(
resp.items,
&scope,
self.audit_ctx.as_deref(),
action_type,
resource_kind,
0,
);
PaginatedResponse {
items: kept,
next_marker: resp.next_marker,
has_more: resp.has_more,
}
}
}

Expand Down Expand Up @@ -100,7 +160,7 @@ impl GlancePort for GlanceHttpAdapter {
pagination: &PaginationParams,
) -> ApiResult<PaginatedResponse<Image>> {
let query = build_image_query(filter, pagination);
paginated_list(
let resp = paginated_list(
&self.base,
"/v2/images",
&query,
Expand All @@ -109,7 +169,12 @@ impl GlancePort for GlanceHttpAdapter {
(resp.images, next)
},
)
.await
.await?;
// BL-P2-091: defense-in-depth refilter atop server-side visibility
// filtering. Glance v2 doesn't accept `tenant_id=<scope>`; we rely
// on `visibility=private/shared/community` plus this response-side
// owner check to fail-safe drop cross-project rows admins may see.
Ok(self.refilter_response(resp, filter.all_tenants, "FetchImages", "image"))
}

async fn get_image(&self, image_id: &str) -> ApiResult<Image> {
Expand Down
46 changes: 46 additions & 0 deletions src/adapter/http/neutron_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub type NeutronAuditCtx = AuditCtx;
pub type NovaAuditCtx = AuditCtx;
/// Step 14 placeholder — Cinder adapter wiring will use this alias.
pub type CinderAuditCtx = AuditCtx;
/// BL-P2-091 — Glance adapter wiring uses this alias.
pub type GlanceAuditCtx = AuditCtx;

/// Bundle of per-service [`AuditCtx`] instances passed to
/// [`crate::adapter::registry::AdapterRegistry::new_http`]. Replaces the
Expand All @@ -84,6 +86,7 @@ pub struct AdapterAuditConfig {
pub neutron: Option<Arc<NeutronAuditCtx>>,
pub nova: Option<Arc<NovaAuditCtx>>,
pub cinder: Option<Arc<CinderAuditCtx>>,
pub glance: Option<Arc<GlanceAuditCtx>>,
}

/// Build an [`AdapterAuditConfig`] from the three pieces every service
Expand Down Expand Up @@ -113,6 +116,7 @@ pub fn build_audit_config(
neutron: Some(make("neutron")),
nova: Some(make("nova")),
cinder: Some(make("cinder")),
glance: Some(make("glance")),
}
}

Expand Down Expand Up @@ -347,6 +351,7 @@ mod tests {
assert!(cfg.neutron.is_none());
assert!(cfg.nova.is_none());
assert!(cfg.cinder.is_none());
assert!(cfg.glance.is_none());
}

#[test]
Expand All @@ -356,6 +361,7 @@ mod tests {
assert!(cfg.neutron.is_none());
assert!(cfg.nova.is_none());
assert!(cfg.cinder.is_none());
assert!(cfg.glance.is_none());
}

#[test]
Expand All @@ -378,4 +384,44 @@ mod tests {
.expect("cinder ctx must be Some when logger is Some");
assert_eq!(cinder.service(), "cinder");
}

// --- BL-P2-091: Glance FR1 wiring ---
// `build_audit_config` must also populate `audit.glance` so registry
// `new_http` can pass it to `GlanceHttpAdapter::with_audit`.

#[test]
fn test_build_audit_config_returns_glance_with_service_glance() {
let dir = TempDir::new().unwrap();
let logger = Arc::new(AuditLogger::new(dir.path().join("audit.log")).unwrap());
let scope: Arc<dyn ScopeProvider> = Arc::new(FixedScope(Some("proj-A".into())));
let cfg = build_audit_config(Some(logger), scope, dummy_actor_ctx());

let glance = cfg
.glance
.expect("glance ctx must be Some when logger is Some (BL-P2-091)");
assert_eq!(
glance.service(),
"glance",
"service discriminator must tag glance"
);
}

#[test]
fn test_glance_with_audit_attaches_ctx_default_none() {
// BL-P2-091 mirror of `test_neutron_with_audit_attaches_ctx_default_none`:
// default `GlanceHttpAdapter` leaves `audit_ctx` None; `with_audit`
// returns `Self` so callers chain via `.with_audit(ctx)`.
let dir = TempDir::new().unwrap();
let ctx = Arc::new(build_ctx(&dir, Some("proj-A")));

// Builder type signature alone is enough — the field is private and
// construction needs a BaseHttpClient. Mirror Neutron's signature
// assertion. (Renamed-by-alias: `NeutronAuditCtx` and
// `GlanceAuditCtx` are both `AuditCtx`; `with_audit` is service-typed.)
use crate::adapter::http::glance::GlanceHttpAdapter;
use crate::adapter::http::neutron_audit::GlanceAuditCtx;
let _builder_signature: fn(GlanceHttpAdapter, Arc<GlanceAuditCtx>) -> GlanceHttpAdapter =
GlanceHttpAdapter::with_audit;
let _ = ctx;
}
}
Loading
Loading