use super::audit::{OrgAuditEntry, OrgAuditEvent};
use super::error::{OrganizationError, Result};
use super::manager::MembershipCreateParams;
use super::seats::{SeatChecker, UnlimitedSeats};
use super::storage::{MembershipStore, OptionalAuditStore, OrgAuditStore, WithAuditStore};
use super::utils::current_timestamp;
use tracing::{debug, info, instrument};
pub struct MembershipManager<M, S = UnlimitedSeats, A = ()>
where
M: MembershipStore,
S: SeatChecker,
A: OptionalAuditStore,
{
membership_store: M,
seat_checker: S,
audit_store: A,
}
impl<M> MembershipManager<M, UnlimitedSeats, ()>
where
M: MembershipStore,
{
#[must_use]
pub fn new_without_seats(membership_store: M) -> Self {
Self {
membership_store,
seat_checker: UnlimitedSeats,
audit_store: (),
}
}
}
impl<M, S> MembershipManager<M, S, ()>
where
M: MembershipStore,
S: SeatChecker,
{
#[must_use]
pub fn new(membership_store: M, seat_checker: S) -> Self {
Self {
membership_store,
seat_checker,
audit_store: (),
}
}
pub fn with_audit_store<AuditStore: OrgAuditStore + Clone + 'static>(
self,
audit_store: AuditStore,
) -> MembershipManager<M, S, WithAuditStore<AuditStore>> {
MembershipManager {
membership_store: self.membership_store,
seat_checker: self.seat_checker,
audit_store: WithAuditStore(audit_store),
}
}
}
impl<M, S, A> MembershipManager<M, S, A>
where
M: MembershipStore,
S: SeatChecker,
A: OptionalAuditStore,
{
pub fn membership_store(&self) -> &M {
&self.membership_store
}
pub fn seat_checker(&self) -> &S {
&self.seat_checker
}
#[instrument(skip(self, membership_factory))]
pub async fn add_member<F>(
&self,
org_id: &str,
user_id: &str,
actor_id: &str,
membership_factory: F,
) -> Result<M::Membership>
where
F: FnOnce(MembershipCreateParams) -> M::Membership,
{
let actor_membership = self
.membership_store
.get_membership(org_id, actor_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let actor_role = self.membership_store.membership_role(&actor_membership);
if !self.membership_store.can_manage_members(&actor_role) {
return Err(OrganizationError::insufficient_permission(
"can_manage_members",
));
}
if self.membership_store.is_member(org_id, user_id).await? {
return Err(OrganizationError::AlreadyMember);
}
let current_count = self.membership_store.count_members(org_id).await?;
if !self
.seat_checker
.has_seat_available(org_id, current_count)
.await?
{
let limit = self
.seat_checker
.get_seat_limit(org_id)
.await?
.unwrap_or(current_count);
return Err(OrganizationError::seat_limit_reached(current_count, limit));
}
let now = current_timestamp();
let membership = membership_factory(MembershipCreateParams {
org_id: org_id.to_string(),
user_id: user_id.to_string(),
is_owner: false,
joined_at: now,
});
self.membership_store.add_member(&membership).await?;
info!(org_id, user_id, actor_id, "Member added");
self.audit_store
.record(
OrgAuditEntry::new(OrgAuditEvent::MemberAdded, org_id, actor_id)
.with_target(user_id),
)
.await;
Ok(membership)
}
#[instrument(skip(self))]
pub async fn remove_member(&self, org_id: &str, user_id: &str, actor_id: &str) -> Result<()> {
let actor_membership = self
.membership_store
.get_membership(org_id, actor_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let actor_role = self.membership_store.membership_role(&actor_membership);
if !self.membership_store.can_manage_members(&actor_role) {
return Err(OrganizationError::insufficient_permission(
"can_manage_members",
));
}
let target_membership = self
.membership_store
.get_membership(org_id, user_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let target_role = self.membership_store.membership_role(&target_membership);
if self.membership_store.is_owner(&target_role) {
return Err(OrganizationError::CannotRemoveOwner);
}
self.membership_store.remove_member(org_id, user_id).await?;
info!(org_id, user_id, actor_id, "Member removed");
self.audit_store
.record(
OrgAuditEntry::new(OrgAuditEvent::MemberRemoved, org_id, actor_id)
.with_target(user_id),
)
.await;
Ok(())
}
#[instrument(skip(self))]
pub async fn leave(&self, org_id: &str, user_id: &str) -> Result<()> {
let membership = self
.membership_store
.get_membership(org_id, user_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let role = self.membership_store.membership_role(&membership);
if self.membership_store.is_owner(&role) {
return Err(OrganizationError::CannotRemoveOwner);
}
self.membership_store.remove_member(org_id, user_id).await?;
info!(org_id, user_id, "Member left organization");
self.audit_store
.record(OrgAuditEntry::new(
OrgAuditEvent::MemberLeft,
org_id,
user_id,
))
.await;
Ok(())
}
#[instrument(skip(self, updater))]
pub async fn update_membership<F>(
&self,
org_id: &str,
user_id: &str,
actor_id: &str,
updater: F,
) -> Result<M::Membership>
where
F: FnOnce(&M::Membership) -> M::Membership,
{
let actor_membership = self
.membership_store
.get_membership(org_id, actor_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let actor_role = self.membership_store.membership_role(&actor_membership);
if !self.membership_store.can_manage_members(&actor_role) {
return Err(OrganizationError::insufficient_permission(
"can_manage_members",
));
}
let current = self
.membership_store
.get_membership(org_id, user_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let current_role = self.membership_store.membership_role(¤t);
if self.membership_store.is_owner(¤t_role) {
return Err(OrganizationError::insufficient_permission(
"cannot modify owner role directly",
));
}
let updated = updater(¤t);
let new_role = self.membership_store.membership_role(&updated);
if self.membership_store.is_owner(&new_role) {
return Err(OrganizationError::insufficient_permission(
"cannot promote to owner via update, use transfer_ownership instead",
));
}
self.membership_store.update_membership(&updated).await?;
debug!(org_id, user_id, actor_id, "Membership updated");
self.audit_store
.record(
OrgAuditEntry::new(OrgAuditEvent::MemberRoleChanged, org_id, actor_id)
.with_target(user_id),
)
.await;
Ok(updated)
}
#[instrument(skip(self, make_owner, demote_owner))]
pub async fn transfer_ownership<F, G>(
&self,
org_id: &str,
new_owner_id: &str,
actor_id: &str,
make_owner: F,
demote_owner: G,
) -> Result<()>
where
F: FnOnce(&M::Membership) -> M::Membership,
G: FnOnce(&M::Membership) -> M::Membership,
{
let actor_membership = self
.membership_store
.get_membership(org_id, actor_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let actor_role = self.membership_store.membership_role(&actor_membership);
if !self.membership_store.can_transfer_ownership(&actor_role) {
return Err(OrganizationError::insufficient_permission(
"can_transfer_ownership",
));
}
let new_owner_membership = self
.membership_store
.get_membership(org_id, new_owner_id)
.await?
.ok_or(OrganizationError::NotMember)?;
let demoted = demote_owner(&actor_membership);
let promoted = make_owner(&new_owner_membership);
self.membership_store
.update_memberships_atomic(&[demoted, promoted])
.await?;
info!(
org_id,
new_owner_id,
former_owner = actor_id,
"Ownership transferred"
);
self.audit_store
.record(
OrgAuditEntry::new(OrgAuditEvent::OwnershipTransferred, org_id, actor_id)
.with_target(new_owner_id),
)
.await;
Ok(())
}
pub async fn get_membership(
&self,
org_id: &str,
user_id: &str,
) -> Result<Option<M::Membership>> {
self.membership_store
.get_membership(org_id, user_id)
.await
.map_err(Into::into)
}
pub async fn list_members(&self, org_id: &str) -> Result<Vec<M::Membership>> {
self.membership_store
.list_members(org_id)
.await
.map_err(Into::into)
}
pub async fn is_member(&self, org_id: &str, user_id: &str) -> Result<bool> {
self.membership_store
.is_member(org_id, user_id)
.await
.map_err(Into::into)
}
pub async fn can_manage_members(&self, org_id: &str, user_id: &str) -> Result<bool> {
let membership = self
.membership_store
.get_membership(org_id, user_id)
.await?;
Ok(membership.is_some_and(|m| {
let role = self.membership_store.membership_role(&m);
self.membership_store.can_manage_members(&role)
}))
}
pub async fn can_manage_settings(&self, org_id: &str, user_id: &str) -> Result<bool> {
let membership = self
.membership_store
.get_membership(org_id, user_id)
.await?;
Ok(membership.is_some_and(|m| {
let role = self.membership_store.membership_role(&m);
self.membership_store.can_manage_settings(&role)
}))
}
pub async fn can_delete_org(&self, org_id: &str, user_id: &str) -> Result<bool> {
let membership = self
.membership_store
.get_membership(org_id, user_id)
.await?;
Ok(membership.is_some_and(|m| {
let role = self.membership_store.membership_role(&m);
self.membership_store.can_delete_org(&role)
}))
}
pub async fn count_members(&self, org_id: &str) -> Result<u32> {
self.membership_store
.count_members(org_id)
.await
.map_err(Into::into)
}
}