systemprompt_security/authz/parent_chain/
mod.rs1mod cache;
16mod sources;
17
18use std::collections::BTreeMap;
19use std::sync::Arc;
20
21use systemprompt_identifiers::{PluginId, UserId};
22
23use super::error::AuthzResult;
24use super::repository::AccessControlRepository;
25use super::resolver::{ResolveInput, ResolveParent, resolve};
26use super::subject::{SubjectAttributes, SubjectDimension};
27use super::types::{AccessRule, Decision, EntityKind, EntityRef};
28
29pub use cache::ChainIndexCache;
30pub use sources::{ChainSources, MarketplaceSource};
31
32#[derive(Debug, Clone)]
33pub struct LoadedParent {
34 pub entity: EntityRef,
35 pub rules: Vec<AccessRule>,
36 pub default_included: Option<bool>,
37}
38
39impl LoadedParent {
40 #[must_use]
41 pub fn as_resolve_parent(&self) -> ResolveParent<'_> {
42 ResolveParent {
43 entity: &self.entity,
44 rules: &self.rules,
45 default_included: self.default_included,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
51pub struct ResolveBase<'a> {
52 pub rules: &'a [AccessRule],
53 pub user_id: &'a UserId,
54 pub user_roles: &'a [String],
55 pub default_included: Option<bool>,
56 pub attributes: &'a SubjectAttributes,
57 pub dimensions: &'a [SubjectDimension],
58}
59
60#[derive(Debug, Clone, Default)]
61pub struct ParentChainIndex {
62 marketplace: Option<LoadedParent>,
63 plugins: BTreeMap<PluginId, LoadedParent>,
64 sources: Arc<ChainSources>,
69}
70
71impl ParentChainIndex {
72 #[must_use]
73 pub const fn from_parts(
74 marketplace: Option<LoadedParent>,
75 plugins: BTreeMap<PluginId, LoadedParent>,
76 sources: Arc<ChainSources>,
77 ) -> Self {
78 Self {
79 marketplace,
80 plugins,
81 sources,
82 }
83 }
84
85 pub async fn load(
86 repo: &AccessControlRepository,
87 sources: Arc<ChainSources>,
88 ) -> AuthzResult<Self> {
89 let marketplace = match sources.marketplace.as_ref() {
90 Some(source) => Some(load_marketplace(repo, source).await?),
91 None => None,
92 };
93
94 let plugin_ids = sources.plugin_ids_to_load();
95 let mut rules = repo
96 .list_rules_bulk(EntityKind::Plugin, &plugin_ids)
97 .await?;
98 let entities = repo
99 .list_entities_bulk(EntityKind::Plugin, &plugin_ids)
100 .await?;
101 let plugins = plugin_ids
102 .into_iter()
103 .map(|id| {
104 let parent = LoadedParent {
105 entity: EntityRef::from_kind_and_id(EntityKind::Plugin, &id),
106 rules: rules.remove(&id).unwrap_or_default(),
107 default_included: entities.get(&id).map(|row| row.default_included),
108 };
109 (PluginId::new(id), parent)
110 })
111 .collect();
112
113 Ok(Self {
114 marketplace,
115 plugins,
116 sources,
117 })
118 }
119
120 #[must_use]
121 pub fn chains_for(&self, kind: EntityKind, id: &str) -> Vec<Vec<ResolveParent<'_>>> {
122 let marketplace = self
123 .marketplace
124 .as_ref()
125 .map(LoadedParent::as_resolve_parent);
126 let marketplace_chain = || {
127 marketplace
128 .map(|parent| vec![vec![parent]])
129 .unwrap_or_default()
130 };
131
132 match kind {
133 EntityKind::Skill => {
134 let owned: Vec<Vec<ResolveParent<'_>>> = self
135 .sources
136 .skill_owners
137 .get(id)
138 .into_iter()
139 .flatten()
140 .filter_map(|owner| self.plugins.get(owner).map(|plugin| (owner, plugin)))
141 .map(|(owner, plugin)| {
142 let mut chain = vec![plugin.as_resolve_parent()];
143 if self.sources.plugins.contains(owner) {
144 chain.extend(marketplace);
145 }
146 chain
147 })
148 .collect();
149 if !owned.is_empty() {
150 return owned;
151 }
152 if self.sources.is_marketplace_member(kind, id) {
153 return marketplace_chain();
154 }
155 Vec::new()
156 },
157 EntityKind::Plugin if self.sources.plugins.contains(id) => marketplace_chain(),
158 EntityKind::Plugin => Vec::new(),
159 _ if self.sources.is_marketplace_member(kind, id) => marketplace_chain(),
160 _ => Vec::new(),
161 }
162 }
163
164 #[must_use]
165 pub fn resolve(&self, kind: EntityKind, id: &str, base: ResolveBase<'_>) -> Decision {
166 let entity = EntityRef::from_kind_and_id(kind, id);
167 let resolve_with = |parents: &[ResolveParent<'_>]| {
168 resolve(ResolveInput {
169 entity: &entity,
170 rules: base.rules,
171 user_id: base.user_id,
172 user_roles: base.user_roles,
173 default_included: base.default_included,
174 parents,
175 attributes: base.attributes,
176 dimensions: base.dimensions,
177 })
178 };
179
180 let mut first_deny = None;
181 for chain in self.chains_for(kind, id) {
182 let decision = resolve_with(&chain);
183 if matches!(decision, Decision::Allow { .. }) {
184 return decision;
185 }
186 first_deny.get_or_insert(decision);
187 }
188 first_deny.unwrap_or_else(|| resolve_with(&[]))
189 }
190}
191
192async fn load_marketplace(
193 repo: &AccessControlRepository,
194 source: &MarketplaceSource,
195) -> AuthzResult<LoadedParent> {
196 let id = source.id.as_str();
197 let rules = repo
198 .list_rules_for_entity(EntityKind::Marketplace, id)
199 .await?;
200 let default_included = repo
201 .get_entity(EntityKind::Marketplace, id)
202 .await?
203 .map(|row| row.default_included)
204 .or(source.fallback_default_included);
205 Ok(LoadedParent {
206 entity: EntityRef::Marketplace(source.id.clone()),
207 rules,
208 default_included,
209 })
210}