Skip to main content

kcode_k1_access_discovery/
lib.rs

1use std::collections::{BTreeSet, HashSet};
2use std::path::Path;
3
4use access_types_v2::{AccessId as DiscoveryAccessId, TxId as DiscoveryTxId};
5use kcode_k1_access_discovery_store::DiscoveryStore;
6use kcode_k1_access_store::StoredAccess;
7use kcode_k1_access_types::{
8    AccessContext, AccessId, AccessPolicy, Authority, GroupId, SubsystemId, Target, TxId, UserId,
9    ViewerSubject,
10};
11
12pub struct AccessDiscovery {
13    store: DiscoveryStore,
14}
15
16impl AccessDiscovery {
17    pub fn open(root: &Path) -> Result<Self, String> {
18        Ok(Self {
19            store: DiscoveryStore::open(root)?,
20        })
21    }
22
23    pub fn checkpoint(&self) -> Result<Option<TxId>, String> {
24        self.store.checkpoint()
25    }
26
27    pub fn complete(&self, records: &[StoredAccess]) -> Result<bool, String> {
28        for record in records {
29            let (users, groups) = subjects(record.policy());
30            if self
31                .store
32                .contains_missing(&users, &groups, discovery_id(record.access_id()))?
33            {
34                return Ok(false);
35            }
36        }
37        Ok(true)
38    }
39
40    pub fn commit(
41        &self,
42        callback_txid: TxId,
43        discovery: Option<(AccessId, &AccessPolicy)>,
44    ) -> Result<(), String> {
45        match discovery {
46            Some((access_id, policy)) => {
47                let (users, groups) = subjects(policy);
48                self.store
49                    .commit(callback_txid, &users, &groups, discovery_id(access_id))
50                    .map(|_| ())
51            }
52            None => self
53                .store
54                .commit(
55                    callback_txid,
56                    &[],
57                    &[],
58                    DiscoveryAccessId::new(DiscoveryTxId::from_bytes(*callback_txid.as_bytes())),
59                )
60                .map(|_| ()),
61        }
62    }
63
64    pub fn missing(
65        &self,
66        context: &AccessContext,
67        records: &[StoredAccess],
68        access_id: AccessId,
69        expected_subsystem: SubsystemId,
70    ) -> Result<bool, String> {
71        let Some(record) = visible_record(context, records, access_id, expected_subsystem) else {
72            return Ok(false);
73        };
74        let (users, groups) = subjects(record.policy());
75        self.store
76            .contains_missing(&users, &groups, discovery_id(access_id))
77    }
78
79    pub fn list_user(
80        &self,
81        context: &AccessContext,
82        records: &[StoredAccess],
83        subsystem: SubsystemId,
84    ) -> Result<Vec<AccessId>, String> {
85        Ok(self
86            .store
87            .list_user(context.user())?
88            .into_iter()
89            .map(current_id)
90            .filter(|access_id| visible_record(context, records, *access_id, subsystem).is_some())
91            .collect())
92    }
93
94    pub fn list_group(
95        &self,
96        context: &AccessContext,
97        records: &[StoredAccess],
98        group: GroupId,
99        subsystem: SubsystemId,
100    ) -> Result<Vec<AccessId>, String> {
101        Ok(self
102            .store
103            .list_group(group)?
104            .into_iter()
105            .map(current_id)
106            .filter(|access_id| visible_record(context, records, *access_id, subsystem).is_some())
107            .collect())
108    }
109
110    pub fn list_user_groups(
111        &self,
112        context: &AccessContext,
113        records: &[StoredAccess],
114        groups: &[GroupId],
115        subsystem: SubsystemId,
116    ) -> Result<Vec<(AccessId, Target)>, String> {
117        let mut discovered = self.store.list_user(context.user())?;
118        for group in groups.iter().copied().collect::<BTreeSet<_>>() {
119            discovered.extend(self.store.list_group(group)?);
120        }
121
122        let mut seen = HashSet::new();
123        let mut visible = Vec::new();
124        for access_id in discovered.into_iter().map(current_id) {
125            let Some(record) = visible_record(context, records, access_id, subsystem) else {
126                continue;
127            };
128            if seen.insert(access_id) {
129                visible.push((access_id, record.target().clone()));
130            }
131        }
132        Ok(visible)
133    }
134
135    pub fn discard(&self) -> Result<(), String> {
136        self.store.discard()
137    }
138}
139
140fn subjects(policy: &AccessPolicy) -> (Vec<UserId>, Vec<GroupId>) {
141    let mut users = Vec::with_capacity(1 + policy.editors().len() + policy.viewers().len());
142    let mut groups = Vec::with_capacity(1 + policy.editors().len() + policy.viewers().len());
143    push_authority(&mut users, &mut groups, policy.authority());
144    for authority in policy.editors() {
145        push_authority(&mut users, &mut groups, *authority);
146    }
147    for viewer in policy.viewers() {
148        match viewer {
149            ViewerSubject::User(user) => users.push(*user),
150            ViewerSubject::Group(group) => groups.push(*group),
151            ViewerSubject::Model(_) => {}
152        }
153    }
154    (users, groups)
155}
156
157fn push_authority(users: &mut Vec<UserId>, groups: &mut Vec<GroupId>, authority: Authority) {
158    match authority {
159        Authority::User(user) => users.push(user),
160        Authority::Group(group) => groups.push(group),
161    }
162}
163
164fn discovery_id(access_id: AccessId) -> DiscoveryAccessId {
165    DiscoveryAccessId::new(DiscoveryTxId::from_bytes(*access_id.txid().as_bytes()))
166}
167
168fn current_id(access_id: DiscoveryAccessId) -> AccessId {
169    AccessId::new(TxId::from_bytes(*access_id.txid().as_bytes()))
170}
171
172fn visible_record<'a>(
173    context: &AccessContext,
174    records: &'a [StoredAccess],
175    access_id: AccessId,
176    subsystem: SubsystemId,
177) -> Option<&'a StoredAccess> {
178    records
179        .iter()
180        .find(|record| record.access_id() == access_id)
181        .filter(|record| record.target().subsystem() == subsystem)
182        .filter(|record| !context.filter().contains(record.policy().authority()))
183}