Skip to main content

systemprompt_models/profile/
security.rs

1//! Profile `security:` block: signing keys, trusted issuers, resource
2//! audiences.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use std::path::PathBuf;
8
9use crate::auth::JwtAudience;
10use serde::{Deserialize, Serialize};
11
12/// Audiences the gateway's grant paths require to be present in
13/// [`SecurityConfig::allowed_resource_audiences`].
14///
15/// These are not RFC 8707 external resource URIs — they are the gateway's own
16/// internal protocol audiences that hardcoded scope guards depend on. The
17/// `client_credentials` grant rejects any `hook:*` scope that is not paired
18/// with `audience=hook`, so a profile that does not opt into the `"hook"`
19/// audience cannot mint plugin hook tokens for the bridge. Profile validation
20/// rejects bootstrap if any entry here is missing, so the error surfaces at
21/// the operator's YAML edit rather than at a downstream tenant's first call.
22pub const GATEWAY_REQUIRED_RESOURCE_AUDIENCES: &[&str] = &["hook"];
23
24/// The resource audiences every generated profile must opt into.
25///
26/// Returns [`GATEWAY_REQUIRED_RESOURCE_AUDIENCES`] as owned strings, so the
27/// setup wizard and the env-driven cloud bootstrap seed the same audiences and
28/// pass [`crate::profile::Profile::validate`] from one source of truth.
29#[must_use]
30pub fn default_resource_audiences() -> Vec<String> {
31    GATEWAY_REQUIRED_RESOURCE_AUDIENCES
32        .iter()
33        .map(|aud| (*aud).to_owned())
34        .collect()
35}
36
37const fn default_allow_registration() -> bool {
38    true
39}
40
41fn default_signing_key_path() -> PathBuf {
42    PathBuf::from("signing_key.pem")
43}
44
45/// Default ID-JAG lifetime in seconds; short by design (draft §6) to bound
46/// replay.
47pub const DEFAULT_ID_JAG_TTL_SECS: i64 = 300;
48
49const fn default_id_jag_ttl_secs() -> i64 {
50    DEFAULT_ID_JAG_TTL_SECS
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
54#[serde(deny_unknown_fields)]
55pub struct SecurityConfig {
56    #[serde(rename = "jwt_issuer")]
57    pub issuer: String,
58
59    #[serde(rename = "jwt_access_token_expiration")]
60    pub access_token_expiration: i64,
61
62    #[serde(rename = "jwt_refresh_token_expiration")]
63    pub refresh_token_expiration: i64,
64
65    #[serde(rename = "jwt_audiences")]
66    pub audiences: Vec<JwtAudience>,
67
68    #[serde(default)]
69    pub allowed_resource_audiences: Vec<String>,
70
71    #[serde(default = "default_allow_registration")]
72    pub allow_registration: bool,
73
74    #[serde(default = "default_signing_key_path")]
75    pub signing_key_path: PathBuf,
76
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub trusted_issuers: Vec<TrustedIssuer>,
79
80    #[serde(default = "default_id_jag_ttl_secs")]
81    pub id_jag_ttl_secs: i64,
82}
83
84/// A federated identity provider trusted for the RFC 8693 token-exchange and
85/// EMA (Enterprise-Managed Authorization) paths.
86///
87/// `audience` holds the value the `IdP` places in `id_token.aud`; for a
88/// Salesforce Connected App that is its `client_id`, **not** a URL.
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
90#[serde(deny_unknown_fields)]
91pub struct TrustedIssuer {
92    pub issuer: String,
93    pub jwks_uri: String,
94    pub audience: String,
95
96    /// Accepted JOSE `typ` header values; empty accepts any.
97    #[serde(default, skip_serializing_if = "Vec::is_empty")]
98    pub typ_allowlist: Vec<String>,
99
100    /// `client_id`/`azp` values accepted on the EMA consume path; empty accepts
101    /// any.
102    #[serde(default, skip_serializing_if = "Vec::is_empty")]
103    pub allowed_client_ids: Vec<String>,
104
105    /// Whether this issuer's `id_token` may seed the EMA ID-JAG issuance path.
106    #[serde(default)]
107    pub can_issue_id_jag: bool,
108}