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>,
71}
72
73impl ParentChainIndex {
74 #[must_use]
75 pub const fn from_parts(
76 marketplaces: BTreeMap<MarketplaceId, LoadedParent>,
77 plugins: BTreeMap<PluginId, LoadedParent>,
78 sources: Arc<ChainSources>,
79 ) -> Self {
80 Self {
81 marketplaces,
82 plugins,
83 sources,
84 }
85 }
86
87 pub async fn load(
88 repo: &AccessControlRepository,
89 sources: Arc<ChainSources>,
90 ) -> AuthzResult<Self> {
91 let marketplaces = load_marketplaces(repo, &sources).await?;
92
93 let plugin_ids = sources.plugin_ids_to_load();
94 let mut rules = repo
95 .list_rules_bulk(EntityKind::Plugin, &plugin_ids)
96 .await?;
97 let entities = repo
98 .list_entities_bulk(EntityKind::Plugin, &plugin_ids)
99 .await?;
100 let plugins = plugin_ids
101 .into_iter()
102 .map(|id| {
103 let parent = LoadedParent {
104 entity: EntityRef::from_kind_and_id(EntityKind::Plugin, &id),
105 rules: rules.remove(&id).unwrap_or_default(),
106 default_included: entities.get(&id).map(|row| row.default_included),
107 };
108 (PluginId::new(id), parent)
109 })
110 .collect();
111
112 Ok(Self {
113 marketplaces,
114 plugins,
115 sources,
116 })
117 }
118
119 #[must_use]
120 pub fn resolve(&self, kind: EntityKind, id: &str, base: ResolveBase<'_>) -> Decision {
121 let entity = EntityRef::from_kind_and_id(kind, id);
122 let resolve_with = |parents: &[ResolveParent<'_>]| {
123 resolve(ResolveInput {
124 entity: &entity,
125 rules: base.rules,
126 user_id: base.user_id,
127 user_roles: base.user_roles,
128 default_included: base.default_included,
129 parents,
130 attributes: base.attributes,
131 dimensions: base.dimensions,
132 })
133 };
134
135 let mut first_deny = None;
136 for chain in self.chains_for(kind, id) {
137 let decision = resolve_with(&chain);
138 if decision.permits() {
139 return decision;
140 }
141 first_deny.get_or_insert(decision);
142 }
143 first_deny.unwrap_or_else(|| resolve_with(&[]))
144 }
145}
146
147async fn load_marketplaces(
148 repo: &AccessControlRepository,
149 sources: &ChainSources,
150) -> AuthzResult<BTreeMap<MarketplaceId, LoadedParent>> {
151 let ids = sources.marketplace_ids_to_load();
152 if ids.is_empty() {
153 return Ok(BTreeMap::new());
154 }
155 let raw: Vec<String> = ids.iter().map(|id| id.as_str().to_owned()).collect();
156 let mut rules = repo.list_rules_bulk(EntityKind::Marketplace, &raw).await?;
157 let entities = repo
158 .list_entities_bulk(EntityKind::Marketplace, &raw)
159 .await?;
160 Ok(ids
161 .into_iter()
162 .map(|id| {
163 let fallback = sources
164 .marketplaces
165 .get(&id)
166 .and_then(|source| source.fallback_default_included);
167 let parent = LoadedParent {
168 entity: EntityRef::Marketplace(id.clone()),
169 rules: rules.remove(id.as_str()).unwrap_or_default(),
170 default_included: entities
171 .get(id.as_str())
172 .map(|row| row.default_included)
173 .or(fallback),
174 };
175 (id, parent)
176 })
177 .collect())
178}