affine_core 0.0.2

AFFiNE primitive core.
Documentation
use super::{
  AccessGrant, AclFacts, AuthorizationDecision, AuthorizationRequest, Decision, DecisionSource, DenialReason,
  DocAction, DocAuthorizationDecision, DocRole, RequestedDocAction, RequestedWorkspaceAction, WorkspaceAction,
  WorkspaceRole,
};

pub fn authorize(request: &AuthorizationRequest, facts: &AclFacts, grant: &AccessGrant) -> AuthorizationDecision {
  if !facts.workspace_exists {
    return missing_workspace(request);
  }
  let assigned_role = facts.assigned_workspace_role.filter(|_| facts.active_member);
  let effective_role = match assigned_role {
    Some(WorkspaceRole::Admin) if !grant.rights.commercial => Some(WorkspaceRole::Member),
    role => role,
  };
  let workspace_preview_decision = super::evaluate_workspace_preview(facts);
  let workspace_preview = request
    .workspace_actions
    .iter()
    .any(|action| matches!(action, RequestedWorkspaceAction::Known(WorkspaceAction::Preview)))
    .then_some(workspace_preview_decision);
  let workspace = request
    .workspace_actions
    .iter()
    .map(|action| workspace_decision(action, effective_role, facts, grant, workspace_preview_decision))
    .collect();
  let docs = request
    .doc_actions
    .iter()
    .map(|requested| {
      let doc = facts.docs.iter().find(|doc| doc.doc_id == requested.doc_id);
      let (role, source) = doc.map_or((None, None), |doc| doc_role(doc, effective_role, facts));
      let preview_decision = super::evaluate_doc_preview(doc, facts.url_preview_enabled);
      let preview = requested
        .actions
        .iter()
        .any(|action| matches!(action, RequestedDocAction::Known(DocAction::Preview)))
        .then_some(preview_decision);
      let decisions = requested
        .actions
        .iter()
        .map(|action| doc_decision(action, role, source, assigned_role, facts, grant, doc, preview_decision))
        .collect();
      DocAuthorizationDecision {
        doc_id: requested.doc_id.clone(),
        effective_role: role,
        preview,
        decisions,
      }
    })
    .collect();
  AuthorizationDecision {
    effective_workspace_role: effective_role,
    workspace_preview,
    workspace,
    docs,
  }
}

fn workspace_decision(
  requested: &RequestedWorkspaceAction,
  role: Option<WorkspaceRole>,
  facts: &AclFacts,
  grant: &AccessGrant,
  preview: super::DocPreviewDecision,
) -> Decision {
  let RequestedWorkspaceAction::Known(action) = requested else {
    return denied(requested.as_str(), DenialReason::UnknownAction);
  };
  let public_external = facts.workspace_public && facts.sharing_enabled;
  let allowed = match action {
    WorkspaceAction::Preview => preview.exposure.is_allowed(),
    _ => {
      role.is_some_and(|role| workspace_role_allows(role, *action))
        || public_external && workspace_role_allows(WorkspaceRole::External, *action)
    }
  };
  if *action == WorkspaceAction::AdministratorsManage && !grant.rights.commercial {
    return denied(action.as_str(), DenialReason::CommercialEntitlementRequired);
  }
  Decision {
    action: action.as_str().to_string(),
    allowed,
    source: allowed.then_some(if role.is_some() {
      DecisionSource::WorkspaceMember
    } else if *action == WorkspaceAction::Preview {
      DecisionSource::WorkspacePreviewPolicy
    } else {
      DecisionSource::WorkspacePolicy
    }),
    denial_reason: None,
  }
}

fn workspace_role_allows(role: WorkspaceRole, action: WorkspaceAction) -> bool {
  let minimum = match action {
    WorkspaceAction::Read | WorkspaceAction::OrganizeRead | WorkspaceAction::PropertiesRead => WorkspaceRole::External,
    WorkspaceAction::Sync
    | WorkspaceAction::CreateDoc
    | WorkspaceAction::UsersRead
    | WorkspaceAction::SettingsRead
    | WorkspaceAction::BlobsUpload
    | WorkspaceAction::Copilot => WorkspaceRole::Member,
    WorkspaceAction::UsersManage
    | WorkspaceAction::AdministratorsManage
    | WorkspaceAction::SettingsUpdate
    | WorkspaceAction::PropertiesCreate
    | WorkspaceAction::PropertiesUpdate
    | WorkspaceAction::PropertiesDelete
    | WorkspaceAction::BlobsManage => WorkspaceRole::Admin,
    WorkspaceAction::Delete | WorkspaceAction::TransferOwner | WorkspaceAction::PaymentManage => WorkspaceRole::Owner,
    WorkspaceAction::Preview => return false,
  };
  role >= minimum
}

fn doc_role(
  doc: &super::DocAclFacts,
  workspace_role: Option<WorkspaceRole>,
  facts: &AclFacts,
) -> (Option<DocRole>, Option<DecisionSource>) {
  let mut candidates = Vec::new();
  match workspace_role {
    Some(WorkspaceRole::Owner) => candidates.push((DocRole::Owner, DecisionSource::InheritedWorkspaceRole)),
    Some(WorkspaceRole::Admin) => candidates.push((DocRole::Manager, DecisionSource::InheritedWorkspaceRole)),
    _ => {}
  }
  if let Some(mut role) = doc.explicit_user_role.filter(|role| *role != DocRole::None) {
    if !facts.active_member {
      role = role.min(DocRole::Editor);
    }
    if facts.active_member || doc.sharing_enabled {
      candidates.push((role, DecisionSource::DocGrant));
    }
  } else if facts.active_member {
    if let Some(role) = doc.member_default_role.filter(|role| *role != DocRole::None) {
      candidates.push((role, DecisionSource::MemberDefaultPolicy));
    }
  }
  if doc.sharing_enabled && doc.visibility_public {
    if let Some(role) = doc.public_role.filter(|role| *role != DocRole::None) {
      candidates.push((role, DecisionSource::PublicPolicy));
    }
  }
  candidates
    .into_iter()
    .max_by_key(|(role, _)| *role)
    .map_or((None, None), |(role, source)| (Some(role), Some(source)))
}

fn doc_decision(
  requested: &RequestedDocAction,
  role: Option<DocRole>,
  source: Option<DecisionSource>,
  assigned_workspace_role: Option<WorkspaceRole>,
  facts: &AclFacts,
  grant: &AccessGrant,
  doc: Option<&super::DocAclFacts>,
  preview: super::DocPreviewDecision,
) -> Decision {
  let RequestedDocAction::Known(action) = requested else {
    return denied(requested.as_str(), DenialReason::UnknownAction);
  };
  let doc_read = role.is_some_and(|role| role >= DocRole::External);
  if *action == DocAction::HistoryRead && (!facts.active_member || !doc_read) {
    return denied(action.as_str(), DenialReason::ActiveMemberRequired);
  }
  if *action == DocAction::AnalyticsRead && (!facts.active_member || !grant.rights.commercial) {
    return denied(action.as_str(), DenialReason::CommercialEntitlementRequired);
  }
  if *action == DocAction::AnalyticsViewersRead {
    let privileged = facts.active_member
      && matches!(
        assigned_workspace_role,
        Some(WorkspaceRole::Admin | WorkspaceRole::Owner)
      );
    return if privileged && grant.rights.commercial && doc_read {
      Decision {
        action: action.as_str().to_string(),
        allowed: true,
        source: Some(DecisionSource::WorkspaceMember),
        denial_reason: None,
      }
    } else {
      denied(action.as_str(), DenialReason::WorkspaceAdminRequired)
    };
  }
  if *action == DocAction::Publish && doc.is_some_and(|doc| !doc.sharing_enabled) {
    return denied(action.as_str(), DenialReason::SharingDisabled);
  }
  if *action == DocAction::Preview {
    return Decision {
      action: action.as_str().to_string(),
      allowed: preview.exposure.is_allowed(),
      source: preview
        .exposure
        .is_allowed()
        .then_some(DecisionSource::DocPreviewPolicy),
      denial_reason: None,
    };
  }
  let allowed = role.is_some_and(|role| doc_role_allows(role, *action));
  Decision {
    action: action.as_str().to_string(),
    allowed,
    source: allowed.then_some(source.unwrap_or(DecisionSource::DocGrant)),
    denial_reason: None,
  }
}

pub fn doc_role_allows(role: DocRole, action: DocAction) -> bool {
  let minimum = match action {
    DocAction::Read | DocAction::Copy | DocAction::PropertiesRead | DocAction::CommentsRead => DocRole::External,
    DocAction::UsersRead | DocAction::Duplicate | DocAction::HistoryRead => DocRole::Reader,
    DocAction::CommentsCreate => DocRole::Commenter,
    DocAction::PropertiesUpdate | DocAction::Update | DocAction::AnalyticsRead => DocRole::Editor,
    DocAction::Publish
    | DocAction::Unpublish
    | DocAction::Trash
    | DocAction::Restore
    | DocAction::Delete
    | DocAction::UsersManage
    | DocAction::CommentsModerate => DocRole::Manager,
    DocAction::TransferOwner => DocRole::Owner,
    DocAction::AnalyticsViewersRead | DocAction::Preview => return false,
  };
  role >= minimum
}

fn missing_workspace(request: &AuthorizationRequest) -> AuthorizationDecision {
  AuthorizationDecision {
    effective_workspace_role: None,
    workspace_preview: None,
    workspace: request
      .workspace_actions
      .iter()
      .map(|action| denied(action.as_str(), DenialReason::WorkspaceNotFound))
      .collect(),
    docs: request
      .doc_actions
      .iter()
      .map(|doc| DocAuthorizationDecision {
        doc_id: doc.doc_id.clone(),
        effective_role: None,
        preview: None,
        decisions: doc
          .actions
          .iter()
          .map(|action| denied(action.as_str(), DenialReason::WorkspaceNotFound))
          .collect(),
      })
      .collect(),
  }
}

fn denied(action: &str, reason: DenialReason) -> Decision {
  Decision {
    action: action.to_string(),
    allowed: false,
    source: None,
    denial_reason: Some(reason),
  }
}

#[cfg(test)]
#[path = "../tests/access_control/permission/tests.rs"]
mod tests;