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