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))]
20pub(crate) const NOT_A_MEMBER: &str = "This account may not sign in here.";
21
22#[derive(Aggregate)]
23pub struct Membership {
24    meta: Meta,
25    #[owner]
26    account: AccountId,
27    area: String,
28    #[cfg(feature = "permissions")]
29    role: Option<crate::role::RoleId>,
30}
31
32#[cfg(feature = "account-data")]
33impl Membership {
34    pub(crate) fn area(&self) -> &str {
35        &self.area
36    }
37}
38
39pub struct Memberships;
40
41impl sofie::http::AreaMembers for Memberships {
42    /// Read per request, so a revoked grant stops the next one.
43    #[cfg(feature = "permissions")]
44    fn grants<'a>(
45        &'a self,
46        read: &'a Read,
47        area: &'static str,
48        principal: sofie::db::Uuid,
49    ) -> sofie::db::BoxFuture<'a, Result<Vec<String>>> {
50        Box::pin(roles::granted(read, area, AccountId::from_uuid(principal)))
51    }
52
53    #[cfg(feature = "tokens")]
54    fn bearer<'a>(
55        &'a self,
56        db: &'a sofie::db::Db,
57        area: &'static str,
58        digest: &'a str,
59    ) -> sofie::db::BoxFuture<'a, Result<Option<sofie::http::Bearer>>> {
60        Box::pin(crate::tokens::bearer(db, area, digest))
61    }
62
63    #[cfg(feature = "shield")]
64    fn sign_in_locked<'a>(
65        &'a self,
66        db: &'a sofie::db::Db,
67        account: sofie::db::Uuid,
68        until: sofie::core::Timestamp,
69    ) -> sofie::db::BoxFuture<'a, Result<()>> {
70        Box::pin(crate::lock::tell_owner(db, account, until))
71    }
72
73    #[cfg(feature = "sessions")]
74    fn session_record<'a>(
75        &'a self,
76        db: &'a sofie::db::Db,
77        session: sofie::db::Uuid,
78        seen: Option<sofie::core::Timestamp>,
79    ) -> sofie::db::BoxFuture<'a, Result<sofie::http::SessionRecord>> {
80        Box::pin(crate::sessions::record(db, session, seen))
81    }
82
83    #[cfg(feature = "sessions")]
84    fn session_topic(&self) -> Option<sofie::db::Topic> {
85        Some(sofie::db::Topic::of::<crate::sessions::AccountSession>())
86    }
87}
88
89/// A membership removed, and with `sessions` the account's sessions in its area ended in
90/// the same commit, so a cookie open there stops at its next request.
91pub struct MembershipRemoved {
92    membership: Membership,
93    #[cfg(feature = "sessions")]
94    ended: crate::sessions::Ended,
95}
96
97/// Read and committed beyond the request's reach, as an area's principal seldom owns its
98/// members' records. Without `sessions`, a session open in the area stays open until its
99/// cookie expires.
100pub async fn remove_membership(
101    read: &Read,
102    account: AccountId,
103    area: &str,
104    now: sofie::core::Instant,
105) -> Result<MembershipRemoved> {
106    let membership = held(read, account, area).await?.ok_or(Error::NotFound)?;
107    #[cfg(not(feature = "sessions"))]
108    let _ = now;
109    Ok(MembershipRemoved {
110        membership,
111        #[cfg(feature = "sessions")]
112        ended: crate::sessions::ending_in(read, account, area, now).await?,
113    })
114}
115
116impl Outcome for MembershipRemoved {
117    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
118        if let Reach::OwnedBy(owner) = plan.reach()
119            && owner != self.membership.account.uuid()
120        {
121            return Err(Error::NotFound);
122        }
123        let dialect = plan.dialect();
124        let sql = format!(
125            "DELETE FROM {} WHERE {} = {}",
126            dialect.qualified(Membership::MODULE, Membership::NAME),
127            dialect.quote("id"),
128            dialect.placeholder(1)
129        );
130        let id = SqlValue::Uuid(self.membership.id().uuid());
131        plan.push_as::<Membership>(Step::statement(sql, vec![id], Some(1)));
132        plan.touched::<Membership>(Some(self.membership.account.uuid()));
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))]
163pub(crate) async fn require(read: &Read, account: AccountId, area: &str) -> Result<()> {
164    held(read, account, area)
165        .await?
166        .map(drop)
167        .ok_or_else(|| Error::Rule(NOT_A_MEMBER.to_owned()))
168}