Skip to content

Commit d817f05

Browse files
authored
Merge pull request #1983 from mattgarmon/provisioning
feat(oagw,types-registry): make tenant_id optional in provisioned entities
2 parents 48ec109 + 0214b3c commit d817f05

7 files changed

Lines changed: 106 additions & 50 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

modules/system/oagw/oagw/src/domain/type_provisioning.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use uuid::Uuid;
1717
#[domain_model]
1818
#[derive(Debug, Clone)]
1919
pub struct ProvisionedUpstream {
20-
pub tenant_id: Uuid,
20+
pub tenant_id: Option<Uuid>,
2121
pub request: CreateUpstreamRequest,
2222
}
2323

@@ -28,7 +28,7 @@ pub struct ProvisionedUpstream {
2828
#[domain_model]
2929
#[derive(Debug, Clone)]
3030
pub struct ProvisionedRoute {
31-
pub tenant_id: Uuid,
31+
pub tenant_id: Option<Uuid>,
3232
pub request: CreateRouteRequest,
3333
}
3434

modules/system/oagw/oagw/src/infra/type_provisioning.rs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ struct MatchRules {
301301
/// Intermediate serde struct for deserializing upstream GTS entity content.
302302
#[derive(Deserialize)]
303303
struct UpstreamPayload {
304-
tenant_id: Uuid,
304+
#[serde(default)]
305+
tenant_id: Option<Uuid>,
305306
server: Server,
306307
protocol: String,
307308
#[serde(default)]
@@ -325,7 +326,8 @@ struct UpstreamPayload {
325326
/// Intermediate serde struct for deserializing route GTS entity content.
326327
#[derive(Deserialize)]
327328
struct RoutePayload {
328-
tenant_id: Uuid,
329+
#[serde(default)]
330+
tenant_id: Option<Uuid>,
329331
upstream_id: String,
330332
#[serde(rename = "match")]
331333
match_rules: MatchRules,
@@ -832,7 +834,7 @@ mod tests {
832834

833835
let upstreams = svc.list_upstreams().await.unwrap();
834836
assert_eq!(upstreams.len(), 1);
835-
assert_eq!(upstreams[0].tenant_id, tenant);
837+
assert_eq!(upstreams[0].tenant_id, Some(tenant));
836838
assert_eq!(upstreams[0].request.id, Some(instance_id));
837839
assert!(upstreams[0].request.enabled);
838840
}
@@ -913,7 +915,7 @@ mod tests {
913915

914916
let routes = svc.list_routes().await.unwrap();
915917
assert_eq!(routes.len(), 1);
916-
assert_eq!(routes[0].tenant_id, tenant);
918+
assert_eq!(routes[0].tenant_id, Some(tenant));
917919
assert_eq!(routes[0].request.id, Some(route_instance_id));
918920
assert_eq!(routes[0].request.upstream_id, upstream_id);
919921
assert!(routes[0].request.enabled);
@@ -1033,7 +1035,7 @@ mod tests {
10331035
let payload: UpstreamPayload = serde_json::from_value(json).unwrap();
10341036
let provisioned = payload.into_provisioned(None);
10351037

1036-
assert_eq!(provisioned.tenant_id, tenant);
1038+
assert_eq!(provisioned.tenant_id, Some(tenant));
10371039
let req = &provisioned.request;
10381040
assert_eq!(req.server.endpoints.len(), 2);
10391041
assert_eq!(req.server.endpoints[0].scheme, domain::Scheme::Https);
@@ -1098,7 +1100,7 @@ mod tests {
10981100
.into_provisioned("gts.cf.core.oagw.route.v1~cf.core.oagw.test.v1", route_uuid)
10991101
.expect("upstream_id should parse");
11001102

1101-
assert_eq!(provisioned.tenant_id, tenant);
1103+
assert_eq!(provisioned.tenant_id, Some(tenant));
11021104
let req = &provisioned.request;
11031105
assert_eq!(req.upstream_id, upstream_id);
11041106
assert_eq!(req.priority, 10);
@@ -1128,6 +1130,67 @@ mod tests {
11281130
assert_eq!(rl.cost, 2);
11291131
}
11301132

1133+
#[test]
1134+
fn deserialize_upstream_without_tenant_id_produces_none() {
1135+
let json = serde_json::json!({
1136+
"server": {
1137+
"endpoints": [{"host": "api.example.com", "port": 443, "scheme": "https"}]
1138+
},
1139+
"protocol": "http"
1140+
});
1141+
let payload: UpstreamPayload = serde_json::from_value(json).unwrap();
1142+
let provisioned = payload.into_provisioned(None);
1143+
assert_eq!(provisioned.tenant_id, None);
1144+
}
1145+
1146+
#[test]
1147+
fn deserialize_route_without_tenant_id_produces_none() {
1148+
let upstream_id = Uuid::new_v4();
1149+
let json = serde_json::json!({
1150+
"upstream_id": upstream_id,
1151+
"match": {
1152+
"http": {
1153+
"methods": ["GET"],
1154+
"path": "/api/test"
1155+
}
1156+
},
1157+
"enabled": true,
1158+
"tags": [],
1159+
"priority": 0
1160+
});
1161+
let payload: RoutePayload = serde_json::from_value(json).unwrap();
1162+
let route_uuid = Uuid::new_v4();
1163+
let provisioned = payload
1164+
.into_provisioned("gts.cf.core.oagw.route.v1~cf.core.oagw.test.v1", route_uuid)
1165+
.unwrap();
1166+
assert_eq!(provisioned.tenant_id, None);
1167+
}
1168+
1169+
#[tokio::test]
1170+
async fn list_upstreams_without_tenant_id_returns_none() {
1171+
let instance_id = Uuid::new_v4();
1172+
let content = serde_json::json!({
1173+
"server": {
1174+
"endpoints": [{"host": "127.0.0.1", "port": 8080, "scheme": "http"}]
1175+
},
1176+
"protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1",
1177+
"enabled": true,
1178+
"tags": []
1179+
});
1180+
let gts_id = format!("gts.cf.core.oagw.upstream.v1~{instance_id}");
1181+
1182+
let registry = Arc::new(
1183+
MockTypesRegistryClient::new()
1184+
.with_instances([make_upstream_instance(&gts_id, content)]),
1185+
);
1186+
let svc = TypeProvisioningServiceImpl::new(registry);
1187+
1188+
let upstreams = svc.list_upstreams().await.unwrap();
1189+
assert_eq!(upstreams.len(), 1);
1190+
assert_eq!(upstreams[0].tenant_id, None);
1191+
assert_eq!(upstreams[0].request.id, Some(instance_id));
1192+
}
1193+
11311194
#[test]
11321195
fn deserialize_missing_field_returns_error() {
11331196
// Missing required "server" field.

modules/system/oagw/oagw/src/module.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub struct AppState {
4646
pub struct OutboundApiGatewayModule {
4747
state: arc_swap::ArcSwapOption<AppState>,
4848
registry_client: OnceLock<Arc<dyn TypesRegistryClient>>,
49+
tenant_resolver: OnceLock<Arc<dyn TenantResolverClient>>,
4950
type_provisioning: OnceLock<Arc<dyn TypeProvisioningService>>,
5051
}
5152

@@ -54,6 +55,7 @@ impl Default for OutboundApiGatewayModule {
5455
Self {
5556
state: arc_swap::ArcSwapOption::from(None),
5657
registry_client: OnceLock::new(),
58+
tenant_resolver: OnceLock::new(),
5759
type_provisioning: OnceLock::new(),
5860
}
5961
}
@@ -88,7 +90,7 @@ impl Module for OutboundApiGatewayModule {
8890
let cp: Arc<dyn ControlPlaneService> = Arc::new(ControlPlaneServiceImpl::new(
8991
upstream_repo,
9092
route_repo,
91-
tenant_resolver,
93+
tenant_resolver.clone(),
9294
policy_enforcer.clone(),
9395
credstore.clone(),
9496
ssrf_guard.clone(),
@@ -193,6 +195,10 @@ impl Module for OutboundApiGatewayModule {
193195
.set(registry)
194196
.map_err(|_| anyhow::anyhow!("TypesRegistryClient already set"))?;
195197

198+
self.tenant_resolver
199+
.set(tenant_resolver)
200+
.map_err(|_| anyhow::anyhow!("TenantResolverClient already set"))?;
201+
196202
let app_state = AppState {
197203
cp,
198204
dp,
@@ -215,6 +221,12 @@ impl SystemCapability for OutboundApiGatewayModule {
215221
.ok_or_else(|| anyhow::anyhow!("TypesRegistryClient not set — init() must run first"))?
216222
.clone();
217223

224+
let tenant_resolver = self
225+
.tenant_resolver
226+
.get()
227+
.ok_or_else(|| anyhow::anyhow!("TenantResolverClient not set — init() must run first"))?
228+
.clone();
229+
218230
let provisioning: Arc<dyn TypeProvisioningService> =
219231
Arc::new(TypeProvisioningServiceImpl::new(registry));
220232

@@ -227,48 +239,62 @@ impl SystemCapability for OutboundApiGatewayModule {
227239
.as_ref()
228240
.clone();
229241

242+
// Resolve root tenant for provisioning context.
243+
let bootstrap_ctx = SecurityContext::builder()
244+
.subject_id(modkit_security::constants::DEFAULT_SUBJECT_ID)
245+
.subject_tenant_id(modkit_security::constants::DEFAULT_TENANT_ID)
246+
.build()?;
247+
let root_tenant_id = tenant_resolver
248+
.get_root_tenant(&bootstrap_ctx)
249+
.await
250+
.map_err(|e| anyhow::anyhow!("Failed to resolve root tenant: {e}"))?
251+
.id
252+
.0;
253+
230254
// -- Materialise upstreams and routes from types-registry --
231255
// GTS instance UUIDs are passed through as `CreateUpstreamRequest.id`
232256
// and `CreateRouteRequest.id`, so OAGW uses the config-provided IDs
233257
// directly. Route `upstream_id` already references the upstream's GTS
234258
// instance UUID, so no remapping is needed.
235259
let upstreams = provisioning.list_upstreams().await?;
236260
for u in &upstreams {
261+
let tenant_id = u.tenant_id.unwrap_or(root_tenant_id);
237262
let ctx = SecurityContext::builder()
238-
.subject_tenant_id(u.tenant_id)
263+
.subject_tenant_id(tenant_id)
239264
.subject_id(modkit_security::constants::DEFAULT_SUBJECT_ID)
240265
.build()?;
241266
let created = app_state
242267
.cp
243268
.create_upstream(&ctx, u.request.clone())
244269
.await
245270
.map_err(|e| {
246-
anyhow::anyhow!("Failed to provision upstream (tenant={}): {e}", u.tenant_id)
271+
anyhow::anyhow!("Failed to provision upstream (tenant={tenant_id}): {e}")
247272
})?;
248273
info!(
249274
id = %created.id,
250-
tenant_id = %u.tenant_id,
275+
tenant_id = %tenant_id,
251276
alias = %created.alias,
252277
"Provisioned upstream from types-registry"
253278
);
254279
}
255280

256281
let routes = provisioning.list_routes().await?;
257282
for r in &routes {
283+
let tenant_id = r.tenant_id.unwrap_or(root_tenant_id);
258284
let ctx = SecurityContext::builder()
259-
.subject_tenant_id(r.tenant_id)
285+
.subject_tenant_id(tenant_id)
260286
.subject_id(modkit_security::constants::DEFAULT_SUBJECT_ID)
261287
.build()?;
262288
let created = app_state
263289
.cp
264290
.create_route(&ctx, r.request.clone())
265291
.await
266292
.map_err(|e| {
267-
anyhow::anyhow!("Failed to provision route (tenant={}): {e}", r.tenant_id)
293+
anyhow::anyhow!("Failed to provision route (tenant={tenant_id}): {e}")
268294
})?;
269295
info!(
270296
id = %created.id,
271-
tenant_id = %r.tenant_id,
297+
tenant_id = %tenant_id,
272298
"Provisioned route from types-registry"
273299
);
274300
}

modules/system/types-registry/types-registry/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ modkit = { workspace = true }
4545
modkit-canonical-errors = { workspace = true, features = ["axum"] }
4646
modkit-gts = { workspace = true }
4747
modkit-macros = { workspace = true }
48-
modkit-security = { workspace = true }
4948
modkit-utils = { workspace = true }
5049

5150
[dev-dependencies]

modules/system/types-registry/types-registry/src/config.rs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
use std::time::Duration;
44

55
use serde::Deserialize;
6-
use uuid::Uuid;
76

87
use crate::infra::cache::{CacheConfig, DEFAULT_CACHE_CAPACITY, DEFAULT_CACHE_TTL};
98

@@ -19,14 +18,6 @@ pub struct TypesRegistryConfig {
1918
/// Default: `["$schema", "gtsTid", "type"]`
2019
pub schema_id_fields: Vec<String>,
2120

22-
/// Default tenant ID injected into static entities that don't specify one.
23-
///
24-
/// When a static entity in `entities` has no `tenant_id` field, this value
25-
/// is automatically inserted before registration. Defaults to
26-
/// `modkit_security::constants::DEFAULT_TENANT_ID`.
27-
#[serde(default = "default_tenant_id")]
28-
pub default_tenant_id: Uuid,
29-
3021
/// Raw GTS entity JSON values to register at startup.
3122
///
3223
/// Each entry must be a valid GTS entity with at least an `$id` (or
@@ -96,16 +87,11 @@ impl SingleCacheSettings {
9687
}
9788
}
9889

99-
fn default_tenant_id() -> Uuid {
100-
modkit_security::constants::DEFAULT_TENANT_ID
101-
}
102-
10390
impl Default for TypesRegistryConfig {
10491
fn default() -> Self {
10592
Self {
10693
entity_id_fields: vec!["$id".to_owned(), "gtsId".to_owned(), "id".to_owned()],
10794
schema_id_fields: vec!["$schema".to_owned(), "gtsTid".to_owned(), "type".to_owned()],
108-
default_tenant_id: default_tenant_id(),
10995
entities: Vec::new(),
11096
local_client: LocalClientSettings::default(),
11197
}
@@ -133,10 +119,6 @@ mod tests {
133119
assert_eq!(cfg.entity_id_fields, vec!["$id", "gtsId", "id"]);
134120
assert_eq!(cfg.schema_id_fields, vec!["$schema", "gtsTid", "type"]);
135121
assert!(cfg.entities.is_empty());
136-
assert_eq!(
137-
cfg.default_tenant_id,
138-
modkit_security::constants::DEFAULT_TENANT_ID
139-
);
140122
}
141123

142124
#[test]

modules/system/types-registry/types-registry/src/module.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ impl Module for TypesRegistryModule {
7575

7676
let gts_config = cfg.to_gts_config();
7777
let static_entities = cfg.entities.clone();
78-
let default_tenant_id = cfg.default_tenant_id;
7978
let type_schemas_cache_cfg = cfg.local_client.cache.type_schemas.to_cache_config();
8079
let instances_cache_cfg = cfg.local_client.cache.instances.to_cache_config();
8180

@@ -105,20 +104,8 @@ impl Module for TypesRegistryModule {
105104

106105
// Register static entities from config (before ready-mode validation)
107106
if !static_entities.is_empty() {
108-
let tenant_id_str = default_tenant_id.to_string();
109-
let entities: Vec<serde_json::Value> = static_entities
110-
.into_iter()
111-
.map(|mut v| {
112-
if let Some(obj) = v.as_object_mut() {
113-
obj.entry("tenant_id")
114-
.or_insert_with(|| serde_json::Value::String(tenant_id_str.clone()));
115-
}
116-
v
117-
})
118-
.collect();
119-
120-
let entity_count = entities.len();
121-
let results = service.register(entities);
107+
let entity_count = static_entities.len();
108+
let results = service.register(static_entities);
122109
let summary = RegisterSummary::from_results(&results);
123110

124111
if !summary.all_succeeded() {

0 commit comments

Comments
 (0)