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