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