Skip to main content

coven_database/
provider_access.rs

1//! Account intentions, confirmed provider state and their durable request (§14.2).
2
3use crate::{sqlite::DatabaseConnection, DbError, NewOperation, OperationId, PendingValue};
4use coven_crypto::SecretBytes;
5
6/// A provider-access operation's steps (§14.2): queued; its request's
7/// target fixed, before storage is called; and an inconclusive request
8/// reported, to be fixed again on retry.
9pub(crate) const REQUEST_QUEUED_STEP: u32 = 0;
10/// The step a provider-access operation reaches once its request's target
11/// is fixed, before storage is called.
12pub const REQUEST_FIXED_STEP: u32 = 1;
13pub(crate) const REQUEST_FAILED_STEP: u32 = 2;
14
15/// One account's intention derived from membership and this device's invitations.
16pub struct ProviderAccessChange {
17    /// Provider account identity.
18    pub account: String,
19    /// Whether a current member or open invite needs the account.
20    pub intended: bool,
21    /// Received access entries describe grants already delivered.
22    pub initially_granted: bool,
23    /// An open invite also needs the provider's acceptance information.
24    pub needs_invitation: bool,
25    /// The journal payload identifying this account.
26    pub operation: NewOperation,
27}
28
29/// A request remains associated with its account until confirmation matches intention.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct ProviderAccessRequest {
32    /// Its stable pending subject.
33    pub operation: OperationId,
34    /// Fixed before sending; absent while ready.
35    pub target: Option<bool>,
36}
37
38/// A request makes confirmation unknown until the provider supplies evidence.
39#[derive(Debug)]
40pub struct ProviderAccess {
41    /// Latest membership and invitation intention.
42    pub intended: bool,
43    /// Confirmed grant, or unknown when a request has no conclusive reply.
44    pub granted: Option<bool>,
45    /// Encoded provider acceptance information from a confirmed grant.
46    pub invitation: Option<SecretBytes>,
47    /// Unfinished work, including a failed request.
48    pub request: Option<ProviderAccessRequest>,
49}
50
51pub(crate) fn read(
52    db: &DatabaseConnection,
53    account: &str,
54) -> Result<Option<ProviderAccess>, DbError> {
55    db.query("SELECT intended,granted,invitation,operation,target FROM _coven_provider_access WHERE account=?1", [account], |row| {
56        let operation: Option<i64> = row.get(3)?;
57        let target: Option<bool> = row.get(4)?;
58        if operation.is_none() && target.is_some() { return Err(crate::write_encoding::damaged(4, rusqlite::types::Type::Integer)); }
59        Ok(ProviderAccess { intended: row.get(0)?, granted: row.get(1)?, invitation: row.get::<_,Option<Vec<u8>>>(2)?.map(SecretBytes::new),
60            request: operation.map(|id| ProviderAccessRequest { operation: OperationId(id), target }) })
61    }).map(|mut rows| rows.pop())
62}
63
64pub(crate) fn finish(
65    db: &DatabaseConnection,
66    account: &str,
67    id: OperationId,
68) -> Result<(), DbError> {
69    db.internal_execute("UPDATE _coven_provider_access SET operation=NULL,target=NULL WHERE account=?1 AND operation=?2", (account,id.0))?;
70    crate::operation::finish(db, id)?;
71    Ok(())
72}
73
74/// Queue the account's request `id` again with no target fixed, ending the
75/// wait its last attempt left.
76pub(crate) fn requeue(
77    db: &DatabaseConnection,
78    account: &str,
79    id: OperationId,
80) -> Result<(), DbError> {
81    crate::operation::set_step(db, id, REQUEST_QUEUED_STEP)?;
82    crate::retry::clear(db, crate::RetrySubject::Operation(id))?;
83    db.internal_execute(
84        "UPDATE _coven_provider_access SET target=NULL WHERE account=?1 AND target IS NOT NULL",
85        [account],
86    )?;
87    Ok(())
88}
89
90/// Fix a request's target to the account's intention before storage is
91/// called. A revoke may remove the direct share supplying acceptance
92/// tokens even when other permissions remain, so neither confirmation is
93/// kept.
94pub(crate) fn fix_target(db: &DatabaseConnection, account: &str) -> Result<(), DbError> {
95    db.internal_execute("UPDATE _coven_provider_access SET granted=NULL,invitation=NULL,target=intended WHERE account=?1", [account])?;
96    Ok(())
97}
98
99/// The provider confirmed `target`, with its acceptance information when
100/// it granted access; the request has no target fixed any more.
101pub(crate) fn confirm(
102    db: &DatabaseConnection,
103    account: &str,
104    target: bool,
105    invitation: Option<&SecretBytes>,
106) -> Result<(), DbError> {
107    db.internal_execute(
108        "UPDATE _coven_provider_access SET granted=?1,invitation=?2,target=NULL WHERE account=?3",
109        (target, invitation.map(SecretBytes::as_bytes), account),
110    )?;
111    Ok(())
112}
113
114/// Called inside the same transaction as membership or an invitation change.
115pub(crate) fn adopt<P: PendingValue>(
116    db: &DatabaseConnection,
117    changes: &[ProviderAccessChange],
118) -> Result<(), DbError> {
119    for change in changes {
120        let before = match read(db, &change.account)? {
121            Some(before) => before,
122            None => {
123                db.internal_execute(
124                    "INSERT INTO _coven_provider_access(account,intended,granted) VALUES(?1,?2,?3)",
125                    (&change.account, change.intended, change.initially_granted),
126                )?;
127                ProviderAccess {
128                    intended: change.intended,
129                    granted: Some(change.initially_granted),
130                    invitation: None,
131                    request: None,
132                }
133            }
134        };
135        db.internal_execute(
136            "UPDATE _coven_provider_access SET intended=?1 WHERE account=?2 AND intended!=?1",
137            (change.intended, &change.account),
138        )?;
139        if let Some(request) = &before.request {
140            let record =
141                crate::operation::find(db, request.operation)?.ok_or(DbError::DamagedDatabase)?;
142            // An in-flight completion must first account for its fixed target.
143            // Unchanged membership also preserves a previously reported failure.
144            if record.last_step == REQUEST_FIXED_STEP
145                || (record.last_step == REQUEST_FAILED_STEP && before.intended == change.intended)
146            {
147                continue;
148            }
149        }
150        let needed = before.granted != Some(change.intended)
151            || (change.intended && change.needs_invitation && before.invitation.is_none());
152        if !needed {
153            if let Some(request) = before.request {
154                finish(db, &change.account, request.operation)?;
155            }
156            continue;
157        }
158        let id = match before.request {
159            Some(request) => {
160                requeue(db, &change.account, request.operation)?;
161                request.operation
162            }
163            None => {
164                let id = crate::operation::insert(db, &change.operation)?;
165                db.internal_execute(
166                    "UPDATE _coven_provider_access SET operation=?1 WHERE account=?2",
167                    (id.0, &change.account),
168                )?;
169                id
170            }
171        };
172        crate::pending::save(db, &P::provider_access(id, change.account.clone(), change.intended))?;
173    }
174    Ok(())
175}
176
177/// Take back accounts whose confirmed grant can no longer be trusted: the
178/// person confirmed that a lost owner device, whose grants may have landed
179/// after this device's revocations, can land nothing more (§20.9). Each
180/// account forgets its confirmed grant and queues a request for its
181/// intention, so a revocation follows any grant that landed before the
182/// confirmation. An account with a request between its fixed target and its
183/// reply fails the call with `ProviderRequestRunning`, and nothing changes:
184/// that request's reply may precede the confirmation.
185pub(crate) fn retake<P: PendingValue>(
186    db: &DatabaseConnection,
187    changes: &[ProviderAccessChange],
188) -> Result<(), DbError> {
189    for change in changes {
190        if let Some(request) = read(db, &change.account)?.and_then(|state| state.request) {
191            let record =
192                crate::operation::find(db, request.operation)?.ok_or(DbError::DamagedDatabase)?;
193            if record.last_step == REQUEST_FIXED_STEP {
194                return Err(DbError::ProviderRequestRunning);
195            }
196            if record.last_step == REQUEST_FAILED_STEP {
197                // A reported failure waits for the app's retry; this
198                // request is that retry.
199                requeue(db, &change.account, request.operation)?;
200            }
201        }
202        db.internal_execute(
203            "INSERT INTO _coven_provider_access(account,intended) VALUES(?1,?2) \
204             ON CONFLICT(account) DO NOTHING",
205            (&change.account, change.intended),
206        )?;
207        db.internal_execute(
208            "UPDATE _coven_provider_access SET granted=NULL,invitation=NULL WHERE account=?1",
209            [&change.account],
210        )?;
211    }
212    adopt::<P>(db, changes)
213}
214
215#[cfg(test)]
216#[path = "provider_access_tests.rs"]
217pub(crate) mod tests;