sofie_identity/membership/
mod.rs1mod 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 #[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
89pub struct MembershipRemoved {
92 membership: Membership,
93 #[cfg(feature = "sessions")]
94 ended: crate::sessions::Ended,
95}
96
97pub 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 #[cfg(feature = "sessions")]
133 self.ended.plan(plan)?;
134 Ok(())
135 }
136
137 #[cfg(feature = "sessions")]
138 fn committed(&mut self, stamp: sofie::db::Stamp) {
139 self.ended.committed(stamp);
140 }
141}
142
143pub(crate) async fn held(
144 read: &Read,
145 account: AccountId,
146 area: &str,
147) -> Result<Option<Membership>> {
148 Membership::query()
149 .where_(Membership::ACCOUNT.eq(account))
150 .where_(Membership::AREA.eq(area.to_owned()))
151 .first(read)
152 .await
153}
154
155#[cfg(any(
157 feature = "sign-in-links",
158 feature = "totp",
159 feature = "passkeys",
160 feature = "oauth"
161))]
162pub(crate) async fn require(read: &Read, account: AccountId, area: &str) -> Result<()> {
163 held(read, account, area)
164 .await?
165 .map(drop)
166 .ok_or_else(|| Error::Rule(NOT_A_MEMBER.to_owned()))
167}