@jpgilldev / steid

1.6 KBRaw
1//! Authorization predicates shared between use cases.
2//!
3//! One answer per question, in one place. A predicate copied into a second use case
4//! is a predicate that will eventually disagree with itself — and an authorization
5//! check that disagrees with itself fails open somewhere.
6
7use crate::domain::{Actor, Organization, Role, repository::MembershipRepository};
8
9use super::error::Result;
10
11/// Whether the actor owns this organisation.
12///
13/// Owner is strictly stronger than membership: a member may read what they can see,
14/// but editing the profile, creating a repository, and pushing are all owner-only.
15/// "Signed in" quietly becoming "allowed" is the usual way this goes wrong.
16pub(crate) async fn is_org_owner(
17 org: &Organization,
18 actor: &Actor,
19 memberships: &impl MembershipRepository,
20) -> Result<bool> {
21 let Some(user_id) = actor.user_id() else {
22 return Ok(false);
23 };
24
25 Ok(memberships
26 .find(&org.id, user_id)
27 .await?
28 .is_some_and(|membership| membership.role == Role::Owner))
29}
30
31/// Whether the actor belongs to this organisation at all, in any role.
32///
33/// The weaker predicate: it gates *seeing* a private repository, where
34/// [`is_org_owner`] gates changing things. Keeping them separate is what stops a
35/// read rule and a write rule from being accidentally satisfied by the same check.
36pub(crate) async fn is_org_member(
37 org: &Organization,
38 actor: &Actor,
39 memberships: &impl MembershipRepository,
40) -> Result<bool> {
41 let Some(user_id) = actor.user_id() else {
42 return Ok(false);
43 };
44
45 Ok(memberships.find(&org.id, user_id).await?.is_some())
46}