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 runs the pure [`resolve`] resolver per id. It is the shared
6//! engine behind per-user catalogue filtering (marketplace manifests, admin
7//! screens); callers supply the subject's attributes and dimensions exactly
8//! as they would to [`resolve`].
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use std::collections::HashSet;
14
15use systemprompt_identifiers::UserId;
16
17use super::error::AuthzResult;
18use super::repository::AccessControlRepository;
19use super::resolver::{ResolveInput, ResolveParent, resolve};
20use super::subject::{SubjectAttributes, SubjectDimension};
21use super::types::{Decision, EntityKind, EntityRef};
22
23/// Inputs to [`allowed_ids`]: one subject, one entity kind, many candidates.
24#[derive(Debug, Clone, Copy)]
25pub struct BulkKeepQuery<'a> {
26    pub user_id: &'a UserId,
27    pub roles: &'a [String],
28    pub kind: EntityKind,
29    pub ids: &'a [String],
30    pub parents: &'a [ResolveParent<'a>],
31    pub attributes: &'a SubjectAttributes,
32    pub dimensions: &'a [SubjectDimension],
33}
34
35pub async fn allowed_ids(
36    repo: &AccessControlRepository,
37    query: BulkKeepQuery<'_>,
38) -> AuthzResult<HashSet<String>> {
39    if query.ids.is_empty() {
40        return Ok(HashSet::new());
41    }
42    let rules = repo.list_rules_bulk(query.kind, query.ids).await?;
43    let entities = repo.list_entities_bulk(query.kind, query.ids).await?;
44    let mut keep = HashSet::with_capacity(query.ids.len());
45    for id in query.ids {
46        let entity_rules = rules.get(id).map_or(&[][..], Vec::as_slice);
47        let default_included = entities.get(id).map(|e| e.default_included);
48        let entity = EntityRef::from_kind_and_id(query.kind, id);
49        let decision = resolve(ResolveInput {
50            entity: &entity,
51            rules: entity_rules,
52            user_id: query.user_id,
53            user_roles: query.roles,
54            default_included,
55            parents: query.parents,
56            attributes: query.attributes,
57            dimensions: query.dimensions,
58        });
59        if matches!(decision, Decision::Allow { .. }) {
60            keep.insert(id.clone());
61        }
62    }
63    Ok(keep)
64}