Skip to main content

systemprompt_security/authz/
keep.rs

1//! Bulk allow-resolution over one entity kind.
2//!
3//! [`allowed_ids`] answers "which of these candidate ids may this subject
4//! see?" in two queries (rules + entity sentinels) instead of a per-id
5//! lookup, then resolves each id through the caller's [`ParentChainIndex`] so
6//! plugin and marketplace rules cascade onto ruleless entities. It is the
7//! shared engine behind per-user catalogue filtering (marketplace manifests,
8//! admin screens); callers supply the subject's attributes and dimensions
9//! exactly as they would to [`resolve`][super::resolver::resolve].
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use std::collections::HashSet;
15
16use systemprompt_identifiers::UserId;
17
18use super::error::AuthzResult;
19use super::parent_chain::{ParentChainIndex, ResolveBase};
20use super::repository::AccessControlRepository;
21use super::subject::{SubjectAttributes, SubjectDimension};
22use super::types::EntityKind;
23
24/// Inputs to [`allowed_ids`]: one subject, one entity kind, many candidates.
25#[derive(Debug, Clone, Copy)]
26pub struct BulkKeepQuery<'a> {
27    pub user_id: &'a UserId,
28    pub roles: &'a [String],
29    pub kind: EntityKind,
30    pub ids: &'a [String],
31    pub chains: &'a ParentChainIndex,
32    pub attributes: &'a SubjectAttributes,
33    pub dimensions: &'a [SubjectDimension],
34}
35
36pub async fn allowed_ids(
37    repo: &AccessControlRepository,
38    query: BulkKeepQuery<'_>,
39) -> AuthzResult<HashSet<String>> {
40    if query.ids.is_empty() {
41        return Ok(HashSet::new());
42    }
43    let rules = repo.list_rules_bulk(query.kind, query.ids).await?;
44    let entities = repo.list_entities_bulk(query.kind, query.ids).await?;
45    let mut keep = HashSet::with_capacity(query.ids.len());
46    for id in query.ids {
47        let entity_rules = rules.get(id).map_or(&[][..], Vec::as_slice);
48        let default_included = entities.get(id).map(|e| e.default_included);
49        let decision = query.chains.resolve(
50            query.kind,
51            id,
52            ResolveBase {
53                rules: entity_rules,
54                user_id: query.user_id,
55                user_roles: query.roles,
56                default_included,
57                attributes: query.attributes,
58                dimensions: query.dimensions,
59            },
60        );
61        if decision.permits() {
62            keep.insert(id.clone());
63        }
64    }
65    Ok(keep)
66}