Skip to main content

sofie_identity/membership/
mod.rs

1mod adding;
2pub(crate) mod factory;
3#[cfg(feature = "permissions")]
4pub(crate) mod roles;
5
6use sofie::core::SqlValue;
7use sofie::db::{Outcome, Plan, Reach, Step};
8use sofie::prelude::*;
9
10pub use adding::{AddingMember, MemberAdded, add_member, add_member_task};
11
12use crate::AccountId;
13
14#[cfg(any(
15    feature = "sign-in-links",
16    feature = "totp",
17    feature = "passkeys",
18    feature = "oauth",
19    feature = "impersonation"
20))]
21pub(crate) const NOT_A_MEMBER: &str = "This account may not sign in here.";
22
23#[derive(Aggregate)]
24pub struct Membership {
25    meta: Meta,
26    #[owner]
27    account: AccountId,
28    area: String,
29    #[cfg(feature = "permissions")]
30    role: Option<crate::role::RoleId>,
31}
32
33#[cfg(feature = "account-data")]
34impl Membership {
35    pub(crate) fn area(&self) -> &str {
36        &self.area
37    }
38}
39
40pub struct Memberships;
41
42impl sofie::http::AreaMembers for Memberships {
43    /// Read per request, so a revoked grant stops the next one.
44    #[cfg(feature = "permissions")]
45    fn grants<'a>(
46        &'a self,
47        read: &'a Read,
48        area: &'static str,
49        principal: sofie::db::Uuid,
50    ) -> sofie::db::BoxFuture<'a, Result<Vec<String>>> {
51        Box::pin(roles::granted(read, area, AccountId::from_uuid(principal)))
52    }
53
54    #[cfg(feature = "tokens")]
55    fn bearer<'a>(
56        &'a self,
57        db: &'a sofie::db::Db,
58        area: &'static str,
59        digest: &'a str,
60    ) -> sofie::db::BoxFuture<'a, Result<Option<sofie::http::Bearer>>> {
61        Box::pin(crate::tokens::bearer(db, area, digest))
62    }
63
64    #[cfg(feature = "shield")]
65    fn sign_in_locked<'a>(
66        &'a self,
67        db: &'a sofie::db::Db,
68        account: sofie::db::Uuid,
69        until: sofie::core::Timestamp,
70    ) -> sofie::db::BoxFuture<'a, Result<()>> {
71        Box::pin(crate::lock::tell_owner(db, account, until))
72    }
73
74    #[cfg(feature = "sessions")]
75    fn session_record<'a>(
76        &'a self,
77        db: &'a sofie::db::Db,
78        session: sofie::db::Uuid,
79        seen: Option<sofie::core::Timestamp>,
80    ) -> sofie::db::BoxFuture<'a, Result<sofie::http::SessionRecord>> {
81        Box::pin(crate::sessions::record(db, session, seen))
82    }
83
84    #[cfg(feature = "sessions")]
85    fn session_topic(&self) -> Option<sofie::db::Topic> {
86        Some(sofie::db::Topic::of::<crate::sessions::AccountSession>())
87    }
88}
89
90/// A membership removed, and with `sessions` the account's sessions in its area ended in
91/// the same commit, so a cookie open there stops at its next request.
92pub struct MembershipRemoved {
93    membership: Membership,
94    #[cfg(feature = "sessions")]
95    ended: Vec<crate::sessions::AccountSession>,
96}
97
98/// Read and committed beyond the request's reach, as an area's principal seldom owns its
99/// members' records. Without `sessions`, a session open in the area stays open until its
100/// cookie expires.
101pub async fn remove_membership(
102    read: &Read,
103    account: AccountId,
104    area: &str,
105    now: sofie::core::Instant,
106) -> Result<MembershipRemoved> {
107    let membership = held(read, account, area).await?.ok_or(Error::NotFound)?;
108    #[cfg(not(feature = "sessions"))]
109    let _ = now;
110    Ok(MembershipRemoved {
111        membership,
112        #[cfg(feature = "sessions")]
113        ended: crate::sessions::ending_in(read, account, area, now).await?,
114    })
115}
116
117impl Outcome for MembershipRemoved {
118    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
119        if let Reach::OwnedBy(owner) = plan.reach()
120            && owner != self.membership.account.uuid()
121        {
122            return Err(Error::NotFound);
123        }
124        let dialect = plan.dialect();
125        let sql = format!(
126            "DELETE FROM {} WHERE {} = {}",
127            dialect.qualified(Membership::MODULE, Membership::NAME),
128            dialect.quote("id"),
129            dialect.placeholder(1)
130        );
131        let id = SqlValue::Uuid(self.membership.id().uuid());
132        plan.push(Step::statement(sql, vec![id], Some(1)));
133        #[cfg(feature = "sessions")]
134        self.ended.plan(plan)?;
135        Ok(())
136    }
137
138    #[cfg(feature = "sessions")]
139    fn committed(&mut self, stamp: sofie::db::Stamp) {
140        self.ended.committed(stamp);
141    }
142}
143
144pub(crate) async fn held(
145    read: &Read,
146    account: AccountId,
147    area: &str,
148) -> Result<Option<Membership>> {
149    Membership::query()
150        .where_(Membership::ACCOUNT.eq(account))
151        .where_(Membership::AREA.eq(area.to_owned()))
152        .first(read)
153        .await
154}
155
156/// For a way of signing in that proved the account but not its membership.
157#[cfg(any(
158    feature = "sign-in-links",
159    feature = "totp",
160    feature = "passkeys",
161    feature = "oauth",
162    feature = "impersonation"
163))]
164pub(crate) async fn require(read: &Read, account: AccountId, area: &str) -> Result<()> {
165    held(read, account, area)
166        .await?
167        .map(drop)
168        .ok_or_else(|| Error::Rule(NOT_A_MEMBER.to_owned()))
169}