use forge_ir::{
ApiKeyLocation, ApiKeyScheme, OAuth2Flow, OAuth2FlowKind, OAuth2Scheme, SecurityRequirement,
SecurityScheme, SecuritySchemeKind,
};
use serde_json::Value as J;
use crate::ctx::Ctx;
use crate::diag;
use crate::pointer::Ptr;
pub(crate) fn walk_components(ctx: &mut Ctx, root: &serde_json::Map<String, J>, ptr: &mut Ptr) {
let Some(J::Object(components)) = root.get("components") else {
return;
};
let Some(J::Object(schemes)) = components.get("securitySchemes") else {
return;
};
ptr.with_token("components", |ptr| {
ptr.with_token("securitySchemes", |ptr| {
for (name, value) in schemes {
ptr.with_token(name, |ptr| {
let mut scheme: Option<SecurityScheme> = None;
crate::ref_walk::with_resolved_object(ctx, value, ptr, |ctx, resolved, ptr| {
scheme = parse_scheme(ctx, name, resolved, ptr);
Some(())
});
if let Some(s) = scheme {
ctx.security_schemes.push(s);
}
});
}
});
});
}
fn parse_scheme(ctx: &mut Ctx, name: &str, value: &J, ptr: &mut Ptr) -> Option<SecurityScheme> {
let map = match value {
J::Object(m) => m,
_ => {
ctx.push_diag(diag::err(
diag::E_INVALID_TYPE,
"security scheme must be an object",
ptr.loc(ctx.file),
));
return None;
}
};
let ty = match map.get("type").and_then(J::as_str) {
Some(t) => t,
None => {
ctx.push_diag(diag::err(
diag::E_MISSING_FIELD,
"security scheme is missing required `type`",
ptr.loc(ctx.file),
));
return None;
}
};
let documentation = map.get("description").and_then(J::as_str).map(String::from);
let deprecated = map.get("deprecated").and_then(J::as_bool).unwrap_or(false);
let extensions = crate::operations::collect_extensions(ctx, map, ptr);
let kind = match ty {
"apiKey" => parse_api_key(ctx, map, ptr)?,
"http" => parse_http(ctx, map, ptr)?,
"mutualTLS" => SecuritySchemeKind::MutualTls,
"oauth2" => parse_oauth2(ctx, name, map, ptr)?,
"openIdConnect" => parse_openid_connect(ctx, name, map, ptr)?,
other => {
ctx.push_diag(diag::warn(
diag::W_UNKNOWN_SECURITY_SCHEME,
format!("unknown security scheme type `{other}`; skipping `{name}`"),
ptr.loc(ctx.file),
));
return None;
}
};
Some(SecurityScheme {
id: crate::sanitize::ident(name),
kind,
documentation,
deprecated,
extensions,
})
}
fn parse_api_key(
ctx: &mut Ctx,
map: &serde_json::Map<String, J>,
ptr: &mut Ptr,
) -> Option<SecuritySchemeKind> {
let Some(name) = map.get("name").and_then(J::as_str) else {
ctx.push_diag(diag::err(
diag::E_MISSING_FIELD,
"apiKey scheme is missing required `name`",
ptr.loc(ctx.file),
));
return None;
};
let location = match map.get("in").and_then(J::as_str) {
Some("header") => ApiKeyLocation::Header,
Some("query") => ApiKeyLocation::Query,
Some("cookie") => ApiKeyLocation::Cookie,
Some(other) => {
ctx.push_diag(diag::err(
diag::E_INVALID_TYPE,
format!("apiKey scheme has unknown `in` value `{other}`"),
ptr.loc(ctx.file),
));
return None;
}
None => {
ctx.push_diag(diag::err(
diag::E_MISSING_FIELD,
"apiKey scheme is missing required `in`",
ptr.loc(ctx.file),
));
return None;
}
};
Some(SecuritySchemeKind::ApiKey(ApiKeyScheme {
name: name.to_string(),
location,
}))
}
fn parse_http(
ctx: &mut Ctx,
map: &serde_json::Map<String, J>,
ptr: &mut Ptr,
) -> Option<SecuritySchemeKind> {
let Some(scheme) = map.get("scheme").and_then(J::as_str) else {
ctx.push_diag(diag::err(
diag::E_MISSING_FIELD,
"http security scheme is missing required `scheme`",
ptr.loc(ctx.file),
));
return None;
};
match scheme.to_ascii_lowercase().as_str() {
"bearer" => {
let bearer_format = map
.get("bearerFormat")
.and_then(J::as_str)
.map(String::from);
Some(SecuritySchemeKind::HttpBearer { bearer_format })
}
"basic" => Some(SecuritySchemeKind::HttpBasic),
other => {
ctx.push_diag(diag::warn(
diag::W_UNKNOWN_SECURITY_SCHEME,
format!(
"http security scheme `{other}` is not yet supported; only `basic` and \
`bearer` are recognised. Skipping."
),
ptr.loc(ctx.file),
));
None
}
}
}
fn parse_openid_connect(
ctx: &mut Ctx,
name: &str,
map: &serde_json::Map<String, J>,
ptr: &mut Ptr,
) -> Option<SecuritySchemeKind> {
let url = match map.get("openIdConnectUrl").and_then(J::as_str) {
Some(u) => u.to_string(),
None => {
ctx.push_diag(diag::err(
diag::E_MISSING_FIELD,
format!(
"security scheme `{name}` of type `openIdConnect` is missing required \
`openIdConnectUrl`"
),
ptr.loc(ctx.file),
));
return None;
}
};
Some(SecuritySchemeKind::OpenIdConnect { url })
}
fn parse_oauth2(
ctx: &mut Ctx,
name: &str,
map: &serde_json::Map<String, J>,
ptr: &mut Ptr,
) -> Option<SecuritySchemeKind> {
let Some(J::Object(flows)) = map.get("flows") else {
ctx.push_diag(diag::err(
diag::E_MISSING_FIELD,
format!("oauth2 scheme `{name}` is missing required `flows`"),
ptr.loc(ctx.file),
));
return None;
};
let mut parsed_flows: Vec<OAuth2Flow> = Vec::new();
ptr.with_token("flows", |ptr| {
for (flow_name, flow_value) in flows {
ptr.with_token(flow_name, |ptr| {
let kind = match flow_name.as_str() {
"implicit" => OAuth2FlowKind::Implicit,
"password" => OAuth2FlowKind::Password,
"clientCredentials" => OAuth2FlowKind::ClientCredentials,
"authorizationCode" => OAuth2FlowKind::AuthorizationCode,
other => {
ctx.push_diag(diag::err(
diag::E_INVALID_TYPE,
format!("oauth2 scheme `{name}` declares unknown flow kind `{other}`"),
ptr.loc(ctx.file),
));
return;
}
};
if let Some(flow) = parse_oauth2_flow(ctx, name, kind, flow_value, ptr) {
parsed_flows.push(flow);
}
});
}
});
if parsed_flows.is_empty() {
ctx.push_diag(diag::err(
diag::E_MISSING_FIELD,
format!("oauth2 scheme `{name}` declared no usable flow"),
ptr.loc(ctx.file),
));
return None;
}
Some(SecuritySchemeKind::Oauth2(OAuth2Scheme {
flows: parsed_flows,
}))
}
fn parse_oauth2_flow(
ctx: &mut Ctx,
scheme_name: &str,
kind: OAuth2FlowKind,
value: &J,
ptr: &mut Ptr,
) -> Option<OAuth2Flow> {
let map = match value {
J::Object(m) => m,
_ => {
ctx.push_diag(diag::err(
diag::E_INVALID_TYPE,
"oauth2 flow definition must be an object",
ptr.loc(ctx.file),
));
return None;
}
};
let authorization_url = map
.get("authorizationUrl")
.and_then(J::as_str)
.map(String::from);
let token_url = map.get("tokenUrl").and_then(J::as_str).map(String::from);
let refresh_url = map.get("refreshUrl").and_then(J::as_str).map(String::from);
let (need_auth_url, need_token_url) = match kind {
OAuth2FlowKind::Implicit => (true, false),
OAuth2FlowKind::Password | OAuth2FlowKind::ClientCredentials => (false, true),
OAuth2FlowKind::AuthorizationCode => (true, true),
};
let auth_missing = need_auth_url && authorization_url.is_none();
let token_missing = need_token_url && token_url.is_none();
if auth_missing || token_missing {
let flow_name = match kind {
OAuth2FlowKind::Implicit => "implicit",
OAuth2FlowKind::Password => "password",
OAuth2FlowKind::ClientCredentials => "clientCredentials",
OAuth2FlowKind::AuthorizationCode => "authorizationCode",
};
let mut missing: Vec<&str> = Vec::new();
if auth_missing {
missing.push("authorizationUrl");
}
if token_missing {
missing.push("tokenUrl");
}
ctx.push_diag(diag::err(
diag::E_OAUTH2_MISSING_URL,
format!(
"oauth2 scheme `{scheme_name}` `{flow_name}` flow is missing required {}",
missing.join(", ")
),
ptr.loc(ctx.file),
));
return None;
}
let mut scopes: Vec<(String, String)> = Vec::new();
if let Some(J::Object(scope_map)) = map.get("scopes") {
for (k, v) in scope_map {
let desc = v.as_str().unwrap_or("").to_string();
scopes.push((k.clone(), desc));
}
scopes.sort_by(|a, b| a.0.cmp(&b.0));
}
let extensions = crate::operations::collect_extensions(ctx, map, ptr);
Some(OAuth2Flow {
kind,
authorization_url,
token_url,
refresh_url,
scopes,
extensions,
})
}
pub(crate) fn parse_requirements(
ctx: &mut Ctx,
value: &J,
ptr: &mut Ptr,
) -> Vec<SecurityRequirement> {
let mut out = Vec::new();
let Some(items) = value.as_array() else {
ctx.push_diag(diag::err(
diag::E_INVALID_TYPE,
"`security` must be an array",
ptr.loc(ctx.file),
));
return out;
};
for (i, entry) in items.iter().enumerate() {
ptr.with_index(i, |ptr| {
let Some(map) = entry.as_object() else {
ctx.push_diag(diag::err(
diag::E_INVALID_TYPE,
"security requirement entry must be an object",
ptr.loc(ctx.file),
));
return;
};
for (scheme_id, scopes_value) in map {
let scopes: Vec<String> = scopes_value
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
out.push(SecurityRequirement {
scheme_id: crate::sanitize::ident(scheme_id),
scopes,
});
}
});
}
out
}