use super::errors::{PolicyStoreError, TrustedIssuerErrorType};
use super::{TokenEntityMetadata, TrustedIssuer};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use url::Url;
#[derive(Debug, Clone)]
pub(super) struct ParsedIssuer {
pub id: String,
pub issuer: TrustedIssuer,
pub filename: String,
}
pub(super) struct IssuerParser;
impl IssuerParser {
pub(super) fn parse_issuer(
content: &str,
filename: &str,
) -> Result<Vec<ParsedIssuer>, PolicyStoreError> {
let json_value: JsonValue =
serde_json::from_str(content).map_err(|e| PolicyStoreError::JsonParsing {
file: filename.to_string(),
source: e,
})?;
let obj = json_value
.as_object()
.ok_or_else(|| PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::NotAnObject,
})?;
let issuer_id = obj
.get("id")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string)
.unwrap_or({
filename
.strip_suffix(".json")
.or_else(|| filename.strip_suffix(".JSON"))
.unwrap_or(filename)
.to_string()
});
let name = obj.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::MissingRequiredField {
issuer_id: issuer_id.clone(),
field: "name".to_string(),
},
}
})?;
let description = obj
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("");
let oidc_endpoint_str = obj
.get("openid_configuration_endpoint") .or_else(|| obj.get("configuration_endpoint")) .and_then(|v| v.as_str())
.ok_or_else(|| PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::MissingRequiredField {
issuer_id: issuer_id.clone(),
field: "openid_configuration_endpoint".to_string(),
},
})?;
let oidc_endpoint =
Url::parse(oidc_endpoint_str).map_err(|e| PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::InvalidOidcEndpoint {
issuer_id: issuer_id.clone(),
url: oidc_endpoint_str.to_string(),
reason: e.to_string(),
},
})?;
let token_metadata = if let Some(metadata_json) = obj.get("token_metadata") {
Self::parse_token_metadata(metadata_json, &issuer_id, filename)?
} else {
HashMap::new()
};
let issuer = TrustedIssuer {
name: name.to_string(),
description: description.to_string(),
oidc_endpoint,
token_metadata,
};
Ok(vec![ParsedIssuer {
id: issuer_id,
issuer,
filename: filename.to_string(),
}])
}
fn parse_token_metadata(
metadata_json: &JsonValue,
issuer_id: &str,
filename: &str,
) -> Result<HashMap<String, TokenEntityMetadata>, PolicyStoreError> {
let metadata_obj =
metadata_json
.as_object()
.ok_or_else(|| PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::TokenMetadataNotAnObject {
issuer_id: issuer_id.to_string(),
},
})?;
let metadata_map: serde_json::Map<String, JsonValue> = metadata_obj.clone();
let mut token_metadata = HashMap::with_capacity(metadata_map.len());
for (token_type, token_config) in metadata_map {
if !token_config.is_object() {
return Err(PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::TokenMetadataEntryNotAnObject {
issuer_id: issuer_id.to_string(),
token_type: token_type.clone(),
},
});
}
let metadata: TokenEntityMetadata =
serde_json::from_value(token_config).map_err(|e| {
PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::MissingRequiredField {
issuer_id: issuer_id.to_string(),
field: format!("token_metadata.{token_type}: {e}"),
},
}
})?;
if metadata.entity_type_name.is_empty() {
return Err(PolicyStoreError::TrustedIssuerError {
file: filename.to_string(),
err: TrustedIssuerErrorType::MissingRequiredField {
issuer_id: issuer_id.to_string(),
field: format!("token_metadata.{token_type}.entity_type_name"),
},
});
}
token_metadata.insert(token_type, metadata);
}
Ok(token_metadata)
}
pub(super) fn validate_issuers(issuers: &[ParsedIssuer]) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let mut seen_ids = HashMap::with_capacity(issuers.len());
for parsed in issuers {
if let Some(existing_file) = seen_ids.get(&parsed.id) {
errors.push(format!(
"Duplicate issuer ID '{}' found in files '{}' and '{}'",
parsed.id, existing_file, parsed.filename
));
} else {
seen_ids.insert(parsed.id.clone(), parsed.filename.clone());
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub(super) fn create_issuer_map(issuers: Vec<ParsedIssuer>) -> HashMap<String, TrustedIssuer> {
let mut issuer_map = HashMap::with_capacity(issuers.len());
for parsed in issuers {
if let std::collections::hash_map::Entry::Vacant(e) =
issuer_map.entry(parsed.id.clone())
{
e.insert(parsed.issuer);
}
}
issuer_map
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_issuer_with_id() {
let content = r#"{
"id": "3af079fa58a915a4d37a668fb874b7a25b70a37c03cf",
"name": "Test Issuer",
"description": "A test OpenID Connect provider",
"configuration_endpoint": "https://accounts.test.com/.well-known/openid-configuration"
}"#;
let result = IssuerParser::parse_issuer(content, "issuer1.json");
assert!(result.is_ok(), "Should parse issuer with id");
let parsed = result.unwrap();
assert_eq!(parsed.len(), 1, "Should have 1 issuer");
assert_eq!(parsed[0].id, "3af079fa58a915a4d37a668fb874b7a25b70a37c03cf");
assert_eq!(parsed[0].issuer.name, "Test Issuer");
assert_eq!(
parsed[0].issuer.description,
"A test OpenID Connect provider"
);
assert_eq!(
parsed[0].issuer.oidc_endpoint.as_str(),
"https://accounts.test.com/.well-known/openid-configuration"
);
}
#[test]
fn test_parse_issuer_without_id() {
let content = r#"{
"name": "Test Issuer",
"description": "A test OpenID Connect provider",
"configuration_endpoint": "https://accounts.test.com/.well-known/openid-configuration"
}"#;
let result = IssuerParser::parse_issuer(content, "test-issuer.json");
assert!(result.is_ok(), "Should parse issuer without explicit id");
let parsed = result.unwrap();
assert_eq!(parsed.len(), 1, "Should have 1 issuer");
assert_eq!(parsed[0].id, "test-issuer"); assert_eq!(parsed[0].issuer.name, "Test Issuer");
}
#[test]
fn test_parse_issuer_with_token_metadata() {
let content = r#"{
"id": "abd948a5665f6050d6e3ba440bd33ec0884234163aa3",
"name": "Jans Server",
"description": "Jans OpenID Connect Provider",
"configuration_endpoint": "https://jans.test/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"trusted": true,
"entity_type_name": "Jans::access_token"
},
"id_token": {
"trusted": true,
"entity_type_name": "Jans::id_token"
}
}
}"#;
let result = IssuerParser::parse_issuer(content, "jans.json");
assert!(result.is_ok(), "Should parse issuer with token metadata");
let parsed = result.unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].id, "abd948a5665f6050d6e3ba440bd33ec0884234163aa3");
assert_eq!(parsed[0].issuer.token_metadata.len(), 2);
let access_token = parsed[0].issuer.token_metadata.get("access_token").unwrap();
assert_eq!(access_token.entity_type_name, "Jans::access_token");
}
#[test]
fn test_parse_issuer_missing_name() {
let content = r#"{
"description": "Missing name field",
"configuration_endpoint": "https://test.com/.well-known/openid-configuration"
}"#;
let result = IssuerParser::parse_issuer(content, "bad.json");
let err = result.expect_err("Should fail on missing name");
assert!(
matches!(
&err,
PolicyStoreError::TrustedIssuerError {
file,
err: TrustedIssuerErrorType::MissingRequiredField { issuer_id, field }
} if file == "bad.json" && issuer_id == "bad" && field == "name"
),
"Expected MissingRequiredField error for name, got: {err:?}"
);
}
#[test]
fn test_parse_issuer_missing_endpoint() {
let content = r#"{
"name": "Test",
"description": "Missing endpoint"
}"#;
let result = IssuerParser::parse_issuer(content, "bad.json");
let err = result.expect_err("Should fail on missing endpoint");
assert!(
matches!(
&err,
PolicyStoreError::TrustedIssuerError {
file,
err: TrustedIssuerErrorType::MissingRequiredField { issuer_id, field }
} if file == "bad.json" && issuer_id == "bad" && field == "openid_configuration_endpoint"
),
"Expected MissingRequiredField error for endpoint, got: {err:?}"
);
}
#[test]
fn test_parse_issuer_with_openid_configuration_endpoint() {
let content = r#"{
"name": "Test Issuer",
"description": "Using canonical field name",
"openid_configuration_endpoint": "https://accounts.test.com/.well-known/openid-configuration"
}"#;
let result = IssuerParser::parse_issuer(content, "issuer2.json");
assert!(
result.is_ok(),
"Should parse with openid_configuration_endpoint"
);
let parsed = result.unwrap();
assert_eq!(
parsed[0].issuer.oidc_endpoint.as_str(),
"https://accounts.test.com/.well-known/openid-configuration"
);
}
#[test]
fn test_parse_issuer_invalid_url() {
let content = r#"{
"name": "Test",
"description": "Invalid URL",
"configuration_endpoint": "not a valid url"
}"#;
let result = IssuerParser::parse_issuer(content, "bad.json");
let err = result.expect_err("Should fail on invalid URL");
assert!(
matches!(
&err,
PolicyStoreError::TrustedIssuerError {
file,
err: TrustedIssuerErrorType::InvalidOidcEndpoint { issuer_id, url, .. }
} if file == "bad.json" && issuer_id == "bad" && url == "not a valid url"
),
"Expected InvalidOidcEndpoint error, got: {err:?}"
);
}
#[test]
fn test_parse_issuer_invalid_json() {
let content = "{ invalid json }";
let result = IssuerParser::parse_issuer(content, "invalid.json");
let err = result.expect_err("Should fail on invalid JSON");
assert!(
matches!(&err, PolicyStoreError::JsonParsing { file, .. } if file == "invalid.json"),
"Expected JsonParsing error, got: {err:?}"
);
}
#[test]
fn test_parse_token_metadata_missing_entity_type() {
let content = r#"{
"name": "Test",
"description": "Test",
"configuration_endpoint": "https://test.com/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"trusted": true
}
}
}"#;
let result = IssuerParser::parse_issuer(content, "bad.json");
let err = result.expect_err("Should fail on missing entity_type_name in token metadata");
assert!(
matches!(&err, PolicyStoreError::TrustedIssuerError { .. }),
"Expected TrustedIssuerError, got: {err:?}"
);
}
#[test]
fn test_validate_issuers_no_duplicates() {
let issuers = vec![
ParsedIssuer {
id: "issuer1".to_string(),
issuer: TrustedIssuer {
name: "Issuer 1".to_string(),
description: "First".to_string(),
oidc_endpoint: Url::parse(
"https://issuer1.com/.well-known/openid-configuration",
)
.unwrap(),
token_metadata: HashMap::from([(
"access_token".to_string(),
TokenEntityMetadata::access_token(),
)]),
},
filename: "file1.json".to_string(),
},
ParsedIssuer {
id: "issuer2".to_string(),
issuer: TrustedIssuer {
name: "Issuer 2".to_string(),
description: "Second".to_string(),
oidc_endpoint: Url::parse(
"https://issuer2.com/.well-known/openid-configuration",
)
.unwrap(),
token_metadata: HashMap::from([(
"id_token".to_string(),
TokenEntityMetadata::id_token(),
)]),
},
filename: "file2.json".to_string(),
},
];
let result = IssuerParser::validate_issuers(&issuers);
assert!(result.is_ok(), "Should have no validation errors");
}
#[test]
fn test_validate_issuers_duplicate_ids() {
let issuers = vec![
ParsedIssuer {
id: "issuer1".to_string(),
issuer: TrustedIssuer {
name: "Issuer 1".to_string(),
description: "First".to_string(),
oidc_endpoint: Url::parse(
"https://issuer1.com/.well-known/openid-configuration",
)
.unwrap(),
token_metadata: HashMap::from([(
"access_token".to_string(),
TokenEntityMetadata::access_token(),
)]),
},
filename: "file1.json".to_string(),
},
ParsedIssuer {
id: "issuer1".to_string(),
issuer: TrustedIssuer {
name: "Issuer 1 Duplicate".to_string(),
description: "Duplicate".to_string(),
oidc_endpoint: Url::parse(
"https://issuer1.com/.well-known/openid-configuration",
)
.unwrap(),
token_metadata: HashMap::from([(
"id_token".to_string(),
TokenEntityMetadata::id_token(),
)]),
},
filename: "file2.json".to_string(),
},
];
let result = IssuerParser::validate_issuers(&issuers);
let errors = result.expect_err("Should detect duplicate issuer IDs");
assert_eq!(errors.len(), 1, "Expected exactly one duplicate error");
assert!(
errors[0].contains("issuer1")
&& errors[0].contains("file1.json")
&& errors[0].contains("file2.json"),
"Error should reference issuer1, file1.json and file2.json, got: {}",
errors[0]
);
}
#[test]
fn test_validate_issuers_no_token_metadata() {
let issuers = vec![ParsedIssuer {
id: "issuer1".to_string(),
issuer: TrustedIssuer {
name: "Issuer 1".to_string(),
description: "No tokens".to_string(),
oidc_endpoint: Url::parse("https://issuer1.com/.well-known/openid-configuration")
.unwrap(),
token_metadata: HashMap::new(),
},
filename: "file1.json".to_string(),
}];
let result = IssuerParser::validate_issuers(&issuers);
result.expect("Should accept issuer with empty token_metadata for JWKS-only use case");
}
#[test]
fn test_create_issuer_map() {
let issuers = vec![
ParsedIssuer {
id: "issuer1".to_string(),
issuer: TrustedIssuer {
name: "Issuer 1".to_string(),
description: "First".to_string(),
oidc_endpoint: Url::parse(
"https://issuer1.com/.well-known/openid-configuration",
)
.unwrap(),
token_metadata: HashMap::from([(
"access_token".to_string(),
TokenEntityMetadata::access_token(),
)]),
},
filename: "file1.json".to_string(),
},
ParsedIssuer {
id: "issuer2".to_string(),
issuer: TrustedIssuer {
name: "Issuer 2".to_string(),
description: "Second".to_string(),
oidc_endpoint: Url::parse(
"https://issuer2.com/.well-known/openid-configuration",
)
.unwrap(),
token_metadata: HashMap::from([(
"id_token".to_string(),
TokenEntityMetadata::id_token(),
)]),
},
filename: "file2.json".to_string(),
},
];
let map = IssuerParser::create_issuer_map(issuers);
assert_eq!(map.len(), 2);
assert!(map.contains_key("issuer1"));
assert!(map.contains_key("issuer2"));
}
}