systemprompt_security/authz/
subject.rs1use std::collections::BTreeMap;
32use std::fmt::Debug;
33use std::sync::Arc;
34
35use async_trait::async_trait;
36use systemprompt_identifiers::UserId;
37
38use super::registry::AuthzHookContext;
39use super::types::RuleType;
40
41pub const USER_PRECEDENCE: u16 = 0;
42pub const ROLE_PRECEDENCE: u16 = 200;
43
44#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SubjectDimension {
50 pub rule_type: RuleType,
51 pub label: &'static str,
52 pub precedence: u16,
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct SubjectAttributes(BTreeMap<RuleType, Vec<String>>);
61
62impl SubjectAttributes {
63 pub const EMPTY: Self = Self(BTreeMap::new());
64
65 #[must_use]
66 pub const fn new() -> Self {
67 Self::EMPTY
68 }
69
70 pub fn insert(&mut self, rule_type: RuleType, values: Vec<String>) {
71 self.0.insert(rule_type, values);
72 }
73
74 #[must_use]
75 pub fn values(&self, rule_type: &RuleType) -> &[String] {
76 self.0.get(rule_type).map_or(&[], Vec::as_slice)
77 }
78
79 #[must_use]
80 pub fn is_empty(&self) -> bool {
81 self.0.is_empty()
82 }
83}
84
85impl FromIterator<(RuleType, Vec<String>)> for SubjectAttributes {
86 fn from_iter<I: IntoIterator<Item = (RuleType, Vec<String>)>>(iter: I) -> Self {
87 Self(iter.into_iter().collect())
88 }
89}
90
91pub static NO_SUBJECT_ATTRIBUTES: SubjectAttributes = SubjectAttributes::EMPTY;
92
93#[async_trait]
105pub trait SubjectAttributeProvider: Send + Sync + Debug {
106 fn dimension(&self) -> SubjectDimension;
107
108 async fn values_for(&self, user_id: &UserId) -> Result<Vec<String>, super::AuthzError>;
109
110 fn default_attribution(&self) -> bool {
111 true
112 }
113}
114
115pub type SharedSubjectAttributeProvider = Arc<dyn SubjectAttributeProvider>;
117
118#[derive(Debug, Clone, Copy)]
123pub struct SubjectProviderRegistration {
124 pub factory: fn(&AuthzHookContext) -> SharedSubjectAttributeProvider,
125}
126
127inventory::collect!(SubjectProviderRegistration);
128
129#[must_use]
130pub fn discover_subject_providers(ctx: &AuthzHookContext) -> Vec<SharedSubjectAttributeProvider> {
131 inventory::iter::<SubjectProviderRegistration>()
132 .map(|reg| (reg.factory)(ctx))
133 .collect()
134}
135
136#[must_use]
137pub fn dimensions_of(providers: &[SharedSubjectAttributeProvider]) -> Vec<SubjectDimension> {
138 providers.iter().map(|p| p.dimension()).collect()
139}
140
141pub async fn gather_subject_attributes(
142 providers: &[SharedSubjectAttributeProvider],
143 user_id: &UserId,
144) -> Result<SubjectAttributes, super::AuthzError> {
145 let mut attributes = SubjectAttributes::new();
146 for provider in providers {
147 attributes.insert(
148 provider.dimension().rule_type,
149 provider.values_for(user_id).await?,
150 );
151 }
152 Ok(attributes)
153}
154
155#[macro_export]
156macro_rules! register_subject_attribute_provider {
157 ($factory:expr) => {
158 ::inventory::submit! {
159 $crate::authz::SubjectProviderRegistration {
160 factory: $factory,
161 }
162 }
163 };
164}
165
166#[derive(Debug, Clone, Default)]
170pub struct SubjectProviderSet(Arc<[SharedSubjectAttributeProvider]>);
171
172impl SubjectProviderSet {
173 #[must_use]
174 pub fn discover(ctx: &AuthzHookContext) -> Self {
175 Self(discover_subject_providers(ctx).into())
176 }
177
178 #[must_use]
179 pub fn from_providers(providers: Vec<SharedSubjectAttributeProvider>) -> Self {
180 Self(providers.into())
181 }
182
183 #[must_use]
184 pub fn find(&self, dimension: &str) -> Option<&SharedSubjectAttributeProvider> {
185 self.0
186 .iter()
187 .find(|provider| provider.dimension().rule_type.as_str() == dimension)
188 }
189
190 #[must_use]
191 pub fn registered_dimensions(&self) -> Vec<RuleType> {
192 self.0.iter().map(|p| p.dimension().rule_type).collect()
193 }
194
195 pub fn iter(&self) -> impl Iterator<Item = &SharedSubjectAttributeProvider> {
196 self.0.iter()
197 }
198}