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 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 #[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
90pub struct MembershipRemoved {
93 membership: Membership,
94 #[cfg(feature = "sessions")]
95 ended: Vec<crate::sessions::AccountSession>,
96}
97
98pub 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#[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}