/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Error;
use async_trait::async_trait;
use maplit::hashmap;
use mononoke_types::RepositoryId;
use sql::Connection;
use sql::Transaction;
use sql_construct::SqlConstruct;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::mononoke_queries;
use sql_ext::SqlConnections;

const DEFAULT_DB_MSG: &str = "Repo is locked in DB";

#[derive(Eq, PartialEq, Debug)]
pub enum RepoLockState {
    Locked(String),
    Unlocked,
}

#[facet::facet]
#[async_trait]
pub trait RepoLock: Send + Sync {
    /// Check whether a repo is locked, which will prevent new commits being pushed.
    async fn check_repo_lock(&self) -> Result<RepoLockState, Error>;
    async fn all_repos_lock(&self) -> Result<HashMap<RepositoryId, RepoLockState>, Error>;
    /// Lock a repo to prevent pushes. This method returns Ok(true) if the repo wasn't previously
    /// locked, Ok(false) if it was and Err(_) if there is an error modifying the lock status.
    async fn set_repo_lock(&self, lock_state: RepoLockState) -> Result<bool, Error>;
}

mononoke_queries! {
    write SetRepoLockStatus(repo_id: RepositoryId, state: u8, reason: Option<&str>) {
        none,
        mysql("INSERT INTO repo_lock (repo_id, state, reason)
               VALUES ({repo_id}, {state}, {reason})
               ON DUPLICATE KEY UPDATE state = {state}, reason = {reason}")

        sqlite("INSERT OR REPLACE INTO repo_lock (repo_id, state, reason)
                VALUES ({repo_id}, {state}, {reason})")
    }

    read GetRepoLockStatus(repo_id: RepositoryId) -> (u8, Option<String>) {
        "SELECT state, reason FROM repo_lock
        WHERE repo_id = {repo_id}"
    }

    read AllReposLockStatus() -> (RepositoryId, u8, Option<String>) {
        "SELECT repo_id, state, reason FROM repo_lock"
    }
}

#[derive(Debug, Clone)]
pub struct SqlRepoLock {
    write_connection: Connection,
    read_connection: Connection,
}

impl SqlConstruct for SqlRepoLock {
    const LABEL: &'static str = "repo-lock";

    const CREATION_QUERY: &'static str = include_str!("../schemas/sqlite-repo-lock.sql");

    fn from_sql_connections(connections: SqlConnections) -> Self {
        Self {
            write_connection: connections.write_connection,
            read_connection: connections.read_connection,
        }
    }
}

impl SqlConstructFromMetadataDatabaseConfig for SqlRepoLock {}

fn convert_sql_state((state, reason): &(u8, Option<String>)) -> Result<RepoLockState, Error> {
    match state {
        0 => Ok(RepoLockState::Unlocked),
        1 => Ok(RepoLockState::Locked(
            reason.clone().unwrap_or_else(|| DEFAULT_DB_MSG.to_string()),
        )),
        _ => Err(anyhow!("Invalid repo lock state: {}", state)),
    }
}

#[derive(Clone, Copy)]
pub struct TransactionRepoLock {
    repo_id: RepositoryId,
}

impl TransactionRepoLock {
    pub fn new(repo_id: RepositoryId) -> Self {
        Self { repo_id }
    }

    pub async fn check_repo_lock_with_transaction(
        &self,
        txn: Transaction,
    ) -> Result<(Transaction, RepoLockState), Error> {
        let (txn, row) = GetRepoLockStatus::query_with_transaction(txn, &self.repo_id)
            .await
            .context("Failed to query repo lock status")?;

        let state = row
            .first()
            .map_or(Ok(RepoLockState::Unlocked), convert_sql_state)?;

        Ok((txn, state))
    }
}

#[derive(Debug, Clone)]
pub struct MutableRepoLock {
    repo_id: RepositoryId,
    sql_repo_lock: SqlRepoLock,
}

impl MutableRepoLock {
    pub fn new(sql_repo_lock: SqlRepoLock, repo_id: RepositoryId) -> Self {
        Self {
            repo_id,
            sql_repo_lock,
        }
    }
}

#[async_trait]
impl RepoLock for MutableRepoLock {
    async fn check_repo_lock(&self) -> Result<RepoLockState, Error> {
        let row = GetRepoLockStatus::query(&self.sql_repo_lock.read_connection, &self.repo_id)
            .await
            .context("Failed to query repo lock status")?;

        row.first()
            .map_or(Ok(RepoLockState::Unlocked), convert_sql_state)
    }

    async fn all_repos_lock(&self) -> Result<HashMap<RepositoryId, RepoLockState>, Error> {
        let rows = AllReposLockStatus::query(&self.sql_repo_lock.read_connection)
            .await
            .context("Failed to query repo lock status")?;

        rows.into_iter()
            .map(|(repo_id, state, reason)| Ok((repo_id, convert_sql_state(&(state, reason))?)))
            .collect()
    }

    async fn set_repo_lock(&self, lock_state: RepoLockState) -> Result<bool, Error> {
        let (state, reason) = match lock_state {
            RepoLockState::Unlocked => (0, None),
            RepoLockState::Locked(reason) => (1, Some(reason)),
        };

        SetRepoLockStatus::query(
            &self.sql_repo_lock.write_connection,
            &self.repo_id,
            &state,
            &reason.as_deref(),
        )
        .await
        .map(|res| res.affected_rows() > 0)
    }
}

#[derive(Debug, Clone)]
pub struct AlwaysLockedRepoLock {
    repo_id: RepositoryId,
    reason: String,
}

impl AlwaysLockedRepoLock {
    pub fn new(repo_id: RepositoryId, reason: String) -> Self {
        Self { repo_id, reason }
    }
}

#[async_trait]
impl RepoLock for AlwaysLockedRepoLock {
    async fn check_repo_lock(&self) -> Result<RepoLockState, Error> {
        Ok(RepoLockState::Locked(self.reason.clone()))
    }

    async fn all_repos_lock(&self) -> Result<HashMap<RepositoryId, RepoLockState>, Error> {
        Ok(hashmap! { self.repo_id => RepoLockState::Locked(self.reason.clone()) })
    }

    async fn set_repo_lock(&self, _: RepoLockState) -> Result<bool, Error> {
        Err(anyhow!("Repo is locked in config and can't be updated"))
    }
}

#[derive(Debug, Clone)]
pub struct AlwaysUnlockedRepoLock {
    repo_id: RepositoryId,
}

impl AlwaysUnlockedRepoLock {
    pub fn new(repo_id: RepositoryId) -> Self {
        Self { repo_id }
    }
}

#[async_trait]
impl RepoLock for AlwaysUnlockedRepoLock {
    async fn check_repo_lock(&self) -> Result<RepoLockState, Error> {
        Ok(RepoLockState::Unlocked)
    }

    async fn all_repos_lock(&self) -> Result<HashMap<RepositoryId, RepoLockState>, Error> {
        Ok(hashmap! { self.repo_id => RepoLockState::Unlocked })
    }

    async fn set_repo_lock(&self, _: RepoLockState) -> Result<bool, Error> {
        Err(anyhow!("Repo is always unlocked and can't be updated"))
    }
}

#[cfg(test)]
mod test {
    use super::*;

    mononoke_queries! {
        write InsertState(repo_id: RepositoryId, state: u8, reason: Option<&str>) {
            none,
            "INSERT OR REPLACE INTO repo_lock (repo_id, state, reason)
            VALUES ({repo_id}, {state}, {reason})"
        }
    }

    #[tokio::test]
    async fn test_locked() -> Result<(), Error> {
        let sql_repo_lock = SqlRepoLock::with_sqlite_in_memory()?;
        let repo_id = RepositoryId::new(0);

        let repo_lock = MutableRepoLock::new(sql_repo_lock, repo_id);

        InsertState::query(
            &repo_lock.sql_repo_lock.clone().write_connection,
            &repo_id,
            &1,
            &Some("reason"),
        )
        .await?;

        assert_eq!(
            repo_lock.check_repo_lock().await?,
            RepoLockState::Locked("reason".to_string())
        );

        Ok(())
    }

    #[tokio::test]
    async fn test_default_to_unlocked() -> Result<(), Error> {
        let sql_repo_lock = SqlRepoLock::with_sqlite_in_memory()?;
        let repo_id = RepositoryId::new(0);

        let repo_lock = MutableRepoLock::new(sql_repo_lock, repo_id);

        assert_eq!(repo_lock.check_repo_lock().await?, RepoLockState::Unlocked);

        Ok(())
    }

    #[tokio::test]
    async fn test_lock_with_other_repo() -> Result<(), Error> {
        let sql_repo_lock = SqlRepoLock::with_sqlite_in_memory()?;
        let repo_id = RepositoryId::new(0);
        let other_repo_id = RepositoryId::new(1);

        let repo_lock = MutableRepoLock::new(sql_repo_lock.clone(), repo_id);
        let other_repo_lock = MutableRepoLock::new(sql_repo_lock, other_repo_id);

        assert!(
            repo_lock
                .set_repo_lock(RepoLockState::Locked("test".into()))
                .await?,
        );
        assert_eq!(
            repo_lock.check_repo_lock().await?,
            RepoLockState::Locked("test".into())
        );
        assert_eq!(
            other_repo_lock.check_repo_lock().await?,
            RepoLockState::Unlocked
        );

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;

use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
use metaconfig_types::Identity;
use permission_checker::AclProvider;
use permission_checker::BoxPermissionChecker;
use permission_checker::MononokeIdentity;
use permission_checker::MononokeIdentitySet;
use permission_checker::PermissionCheckerBuilder;
use slog::trace;
use slog::Logger;
use tokio::join;
use tunables::tunables;

/// Repository permissions checks
///
/// Perform checks against the various access control lists associated with
/// the repository.
#[facet::facet]
#[mockall::automock]
#[async_trait]
pub trait RepoPermissionChecker: Send + Sync + 'static {
    /// Check whether the given identities are permitted to **read** the
    /// repository.
    async fn check_if_read_access_allowed(&self, identities: &MononokeIdentitySet) -> bool;

    /// Check whether the given identities are premitted to **read** any of
    /// the regions of the repository.
    async fn check_if_any_region_read_access_allowed(
        &self,
        identities: &MononokeIdentitySet,
    ) -> bool;

    async fn check_if_region_read_access_allowed<'a>(
        &'a self,
        region_hipster_acls: &'a [&'a str],
        identities: &'a MononokeIdentitySet,
    ) -> bool;

    /// Check whether the given identities are permitted to make **draft**
    /// changes to the repository.  This means creating commit cloud commits
    /// and modifying scratch bookmarks.
    async fn check_if_draft_access_allowed(&self, identities: &MononokeIdentitySet) -> bool;

    /// Check whether the given identities are permitted to make **public**
    /// changes to the repository.  This means modifying public bookmarks.
    async fn check_if_write_access_allowed(&self, identities: &MononokeIdentitySet) -> bool;

    /// Check whether the given identities are permitted to **bypass the
    /// read-only state** of the repository.
    async fn check_if_read_only_bypass_allowed(&self, identities: &MononokeIdentitySet) -> bool;

    /// Check whether the given identities are permitted to **act as a
    /// service** to make modifications to the repository.  This means
    /// making any modification to the repository that the named service
    /// is permitted to make.
    async fn check_if_service_writes_allowed(
        &self,
        identities: &MononokeIdentitySet,
        service_name: &str,
    ) -> bool;

    /// This is helper function that allows us to gradually migrate towards
    /// using draft permissions for draft access rather than using read
    /// permissions.
    ///
    /// In the future it should be replaced by check_if_draft_access_allowed
    /// from repo permission checker.
    async fn check_if_draft_access_allowed_with_tunable_enforcement(
        &self,
        ctx: &CoreContext,
        identities: &MononokeIdentitySet,
    ) -> bool {
        let log = tunables().log_draft_acl_failures().unwrap_or_default();
        let enforce = tunables().enforce_draft_acl().unwrap_or_default();
        if log || enforce {
            let (draft_result, write_result) = join!(
                self.check_if_draft_access_allowed(identities),
                self.check_if_write_access_allowed(identities)
            );
            // All-repo write access implies draft access.
            let result = draft_result || write_result;
            if !result {
                ctx.scuba().clone().log_with_msg(
                    if enforce {
                        "Repo draft ACL check failed"
                    } else {
                        "Repo draft ACL check would fail"
                    },
                    None,
                );
            }
            if enforce {
                return result;
            }
        }
        self.check_if_read_access_allowed(identities).await
    }
}

pub struct ProdRepoPermissionChecker {
    repo_permchecker: BoxPermissionChecker,
    service_permchecker: BoxPermissionChecker,
    repo_region_permcheckers: HashMap<String, BoxPermissionChecker>,
}

impl ProdRepoPermissionChecker {
    pub async fn new(
        logger: &Logger,
        acl_provider: &dyn AclProvider,
        repo_hipster_acl: Option<&str>,
        service_hipster_acl: Option<&str>,
        repo_region_hipster_acls: Vec<&str>,
        reponame: &str,
        global_allowlist: &[Identity],
    ) -> Result<Self> {
        let mut repo_permchecker_builder = PermissionCheckerBuilder::new();
        if let Some(acl_name) = repo_hipster_acl {
            repo_permchecker_builder = repo_permchecker_builder.allow(
                acl_provider.repo_acl(acl_name).await.with_context(|| {
                    format!("Failed to create repo PermissionChecker for {}", acl_name)
                })?,
            );
        }
        if !global_allowlist.is_empty() {
            let mut allowlisted_identities = MononokeIdentitySet::new();

            for Identity { id_type, id_data } in global_allowlist {
                allowlisted_identities.insert(MononokeIdentity::new(id_type, id_data));
            }

            trace!(logger, "Adding global allowlist for repo {}", reponame);
            repo_permchecker_builder =
                repo_permchecker_builder.allow_allowlist(allowlisted_identities);
        };
        let repo_permchecker = repo_permchecker_builder.build();
        let service_permchecker = if let Some(acl_name) = service_hipster_acl {
            PermissionCheckerBuilder::new()
                .allow(acl_provider.tier_acl(acl_name).await.with_context(|| {
                    format!("Failed to create PermissionChecker for {}", acl_name)
                })?)
                .build()
        } else {
            // If no service tier is set we allow anyone to act as a service
            // (this happens in integration tests).
            PermissionCheckerBuilder::new().allow_all().build()
        };
        let mut repo_region_permcheckers = HashMap::new();
        for acl_name in repo_region_hipster_acls {
            if !repo_region_permcheckers.contains_key(acl_name) {
                let permchecker = PermissionCheckerBuilder::new()
                    .allow(
                        acl_provider
                            .repo_region_acl(acl_name)
                            .await
                            .with_context(|| {
                                format!(
                                    "Failed to create repo region PermissionChecker for {}",
                                    acl_name
                                )
                            })?,
                    )
                    .build();
                repo_region_permcheckers.insert(acl_name.to_string(), permchecker);
            }
        }

        Ok(Self {
            repo_permchecker,
            service_permchecker,
            repo_region_permcheckers,
        })
    }
}

#[async_trait]
impl RepoPermissionChecker for ProdRepoPermissionChecker {
    async fn check_if_read_access_allowed(&self, identities: &MononokeIdentitySet) -> bool {
        self.repo_permchecker.check_set(identities, &["read"]).await
    }

    async fn check_if_any_region_read_access_allowed(
        &self,
        identities: &MononokeIdentitySet,
    ) -> bool {
        for checker in self.repo_region_permcheckers.values() {
            if checker.check_set(identities, &["read"]).await {
                return true;
            }
        }
        false
    }

    async fn check_if_region_read_access_allowed<'a>(
        &'a self,
        region_hipster_acls: &'a [&'a str],
        identities: &'a MononokeIdentitySet,
    ) -> bool {
        for acl in region_hipster_acls {
            if let Some(checker) = self.repo_region_permcheckers.get(*acl) {
                if checker.check_set(identities, &["read"]).await {
                    return true;
                }
            }
        }
        false
    }

    async fn check_if_draft_access_allowed(&self, identities: &MononokeIdentitySet) -> bool {
        self.repo_permchecker
            .check_set(identities, &["draft"])
            .await
    }

    async fn check_if_write_access_allowed(&self, identities: &MononokeIdentitySet) -> bool {
        self.repo_permchecker
            .check_set(identities, &["write"])
            .await
    }

    async fn check_if_read_only_bypass_allowed(&self, identities: &MononokeIdentitySet) -> bool {
        self.repo_permchecker
            .check_set(identities, &["bypass_readonly"])
            .await
    }

    async fn check_if_service_writes_allowed(
        &self,
        identities: &MononokeIdentitySet,
        service_name: &str,
    ) -> bool {
        self.service_permchecker
            .check_set(identities, &[service_name])
            .await
    }
}

pub struct AlwaysAllowRepoPermissionChecker {}

impl AlwaysAllowRepoPermissionChecker {
    pub fn new() -> Self {
        Self {}
    }
}

#[async_trait]
impl RepoPermissionChecker for AlwaysAllowRepoPermissionChecker {
    async fn check_if_read_access_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        true
    }

    async fn check_if_any_region_read_access_allowed(
        &self,
        _identities: &MononokeIdentitySet,
    ) -> bool {
        true
    }

    async fn check_if_region_read_access_allowed<'a>(
        &'a self,
        _region_hipster_acls: &'a [&'a str],
        _identities: &'a MononokeIdentitySet,
    ) -> bool {
        true
    }

    async fn check_if_draft_access_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        true
    }

    async fn check_if_write_access_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        true
    }

    async fn check_if_read_only_bypass_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        true
    }

    async fn check_if_service_writes_allowed(
        &self,
        _identities: &MononokeIdentitySet,
        _service_name: &str,
    ) -> bool {
        true
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;

use anyhow::Result;
use metaconfig_types::RemoteDatabaseConfig;
use metaconfig_types::RemoteMetadataDatabaseConfig;
use mononoke_types::ChangesetId;
use sql::Connection;
use sql_construct::SqlConstruct;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::mononoke_queries;
use sql_ext::SqlConnections;

mononoke_queries! {
    read GetProfilesSize(
        cs_id: ChangesetId,
        >list profiles: String
    ) -> (String, u64) {
        "SELECT profile_name, size
          FROM sparse_profiles_sizes
          WHERE cs_id = {cs_id}
          AND profile_name in {profiles}"
    }

    write InsertProfilesSizes(
        values: (cs_id: ChangesetId, profile: String, size: u64),
    ) {
        insert_or_ignore,
        "{insert_or_ignore} INTO sparse_profiles_sizes
         (cs_id, profile_name, size) VALUES {values}"
    }
}

#[facet::facet]
pub struct RepoSparseProfiles {
    pub sql_profile_sizes: Option<SqlSparseProfilesSizes>,
}

impl RepoSparseProfiles {
    pub fn new(sql_profile_sizes: Option<SqlSparseProfilesSizes>) -> Self {
        Self { sql_profile_sizes }
    }

    pub async fn get_profiles_sizes(
        &self,
        cs_id: ChangesetId,
        profiles: Vec<String>,
    ) -> Result<Option<Vec<(String, u64)>>> {
        Ok(match &self.sql_profile_sizes {
            None => None,
            Some(sql) => Some(sql.get_profiles_sizes(cs_id, profiles).await?),
        })
    }

    pub async fn insert_profiles_sizes(
        &self,
        cs_id: ChangesetId,
        size_map: HashMap<String, u64>,
    ) -> Result<Option<bool>> {
        Ok(match &self.sql_profile_sizes {
            None => None,
            Some(sql) => Some(sql.insert_profiles_sizes(cs_id, size_map).await?),
        })
    }
}

pub struct SqlSparseProfilesSizes {
    write_connection: Connection,
    read_connection: Connection,
}

impl SqlConstruct for SqlSparseProfilesSizes {
    const LABEL: &'static str = "sparse_profiles_sizes";

    const CREATION_QUERY: &'static str =
        include_str!("../schemas/sqlite-sparse-profiles-sizes.sql");

    fn from_sql_connections(connections: SqlConnections) -> Self {
        Self {
            write_connection: connections.write_connection,
            read_connection: connections.read_connection,
        }
    }
}

impl SqlConstructFromMetadataDatabaseConfig for SqlSparseProfilesSizes {
    fn remote_database_config(
        remote: &RemoteMetadataDatabaseConfig,
    ) -> Option<&RemoteDatabaseConfig> {
        Some(&remote.sparse_profiles)
    }
}

impl SqlSparseProfilesSizes {
    pub async fn get_profiles_sizes(
        &self,
        cs_id: ChangesetId,
        profiles: Vec<String>,
    ) -> Result<Vec<(String, u64)>> {
        GetProfilesSize::query(&self.read_connection, &cs_id, &profiles[..]).await
    }

    pub async fn insert_profiles_sizes(
        &self,
        cs_id: ChangesetId,
        size_map: HashMap<String, u64>,
    ) -> Result<bool> {
        let v: Vec<_> = size_map
            .iter()
            .map(|(profile, size)| (&cs_id, profile, size))
            .collect();
        InsertProfilesSizes::query(&self.write_connection, &v[..])
            .await
            .map(|res| res.affected_rows() > 0)
    }
}

#[cfg(test)]
mod test {
    use mononoke_types_mocks::changesetid::ONES_CSID;
    use mononoke_types_mocks::changesetid::THREES_CSID;
    use mononoke_types_mocks::changesetid::TWOS_CSID;

    use super::*;

    #[tokio::test]
    async fn test_simple() -> Result<()> {
        let sql = SqlSparseProfilesSizes::with_sqlite_in_memory()?;

        let mut size = 10;
        for cs in &[ONES_CSID, THREES_CSID, TWOS_CSID] {
            let m: HashMap<_, _> = ["one", "two", "three"]
                .iter()
                .map(|profile| {
                    size += 5;
                    (profile.to_string(), size)
                })
                .collect();
            sql.insert_profiles_sizes(*cs, m).await?;
        }
        let rows = sql
            .get_profiles_sizes(TWOS_CSID, vec!["one".to_string(), "three".to_string()])
            .await?;
        assert_eq!(
            rows,
            vec![("one".to_string(), 45), ("three".to_string(), 55)]
        );
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! SQL query config.
//!
//! Retries and caching option for generic SQL queries.

use caching_ext::CacheHandlerFactory;
use memcache::KeyGen;

pub struct CachingConfig {
    pub keygen: KeyGen,
    pub cache_handler_factory: CacheHandlerFactory,
}

/// SQL query config.
#[facet::facet]
pub struct SqlQueryConfig {
    pub caching: Option<CachingConfig>,
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use acl_regions::AclRegionsRef;
use anyhow::anyhow;
use anyhow::Result;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkKind;
use context::CoreContext;
use metaconfig_types::RepoConfigRef;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use repo_bookmark_attrs::RepoBookmarkAttrsRef;
use repo_permission_checker::RepoPermissionCheckerRef;

use crate::error::AuthorizationError;
use crate::error::DeniedAction;
use crate::error::PermissionDenied;

const GIT_IMPORT_SVC_WRITE_METHOD: &str = "git_import_operations";

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum AuthorizationContext {
    /// Access is always granted.  Should only be used by internal tools and
    /// tests.
    FullAccess,

    /// Access is granted based on the caller's identity.
    Identity,

    /// Access is granted only for reads. All write and draft operations are forbidden.
    ReadOnlyIdentity,

    /// Access is granted for reads and draft operations. Public writes are forbidden.
    /// Represents off-VPN access.
    DraftOnlyIdentity,

    /// Access is granted based on the caller acting as a named service.
    Service(String),
}

impl AuthorizationContext {
    /// Create a new authorization context.
    ///
    /// This context will use the user's identity to check whether they are
    /// authorized to perform each action.
    pub fn new(ctx: &CoreContext) -> AuthorizationContext {
        // The order matters here since read-only is more restrictive than draft-only.
        if ctx.session().is_readonly() {
            AuthorizationContext::ReadOnlyIdentity
        } else if ctx.session().metadata().client_untrusted() {
            AuthorizationContext::DraftOnlyIdentity
        } else {
            AuthorizationContext::Identity
        }
    }

    /// Create a new authorization context.
    ///
    /// This context will use the user's identity to check whether they are
    /// permitted to act as the named service, and then check the service
    /// is permitted to perform each action.
    pub fn new_for_service_writes(service_name: impl Into<String>) -> AuthorizationContext {
        AuthorizationContext::Service(service_name.into())
    }

    /// Create a new authorization context.
    ///
    /// This context will permit all actions, and so must only be used in
    /// internal tools and tests.
    pub fn new_bypass_access_control() -> AuthorizationContext {
        AuthorizationContext::FullAccess
    }

    /// Returns true if this context is for a service write.
    pub fn is_service(&self) -> bool {
        matches!(self, AuthorizationContext::Service(_))
    }

    /// Create a permission denied error for a particular action.
    fn permission_denied(
        &self,
        ctx: &CoreContext,
        denied_action: DeniedAction,
    ) -> AuthorizationError {
        AuthorizationError::from(PermissionDenied {
            denied_action,
            context: self.clone(),
            identities: ctx.metadata().identities().clone(),
        })
    }

    /// Check if user has read access to the full repo.
    pub async fn check_full_repo_read(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoPermissionCheckerRef,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::Identity
            | AuthorizationContext::ReadOnlyIdentity
            | AuthorizationContext::DraftOnlyIdentity
            | AuthorizationContext::Service(_) => {
                // Check the caller's identity permits read access.  Acting as
                // a service does not change read access, so we check the
                // identity in this case also.
                repo.repo_permission_checker()
                    .check_if_read_access_allowed(ctx.metadata().identities())
                    .await
            }
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that the user has read access to the full repo.
    pub async fn require_full_repo_read(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoPermissionCheckerRef,
    ) -> Result<(), AuthorizationError> {
        self.check_full_repo_read(ctx, repo)
            .await
            .permitted_or_else(|| self.permission_denied(ctx, DeniedAction::FullRepoRead))
    }

    /// Check if user has read access to the repo metadata.
    ///
    /// The repo metadata is the bookmarks and changesets, but not the
    /// manifests or file contents.
    pub async fn check_repo_metadata_read(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoPermissionCheckerRef,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::Identity
            | AuthorizationContext::ReadOnlyIdentity
            | AuthorizationContext::DraftOnlyIdentity
            | AuthorizationContext::Service(_) => {
                // Check the caller's identity permits read access.  Acting as
                // a service does not change read access, so we check the
                // identity in this case also.
                repo.repo_permission_checker()
                    .check_if_read_access_allowed(ctx.metadata().identities())
                    .await ||
                // Check if the caller can access via path ACLs.
                repo.repo_permission_checker()
                    .check_if_any_region_read_access_allowed(ctx.metadata().identities())
                    .await
            }
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that the user has read access to the repo metadata.
    pub async fn require_repo_metadata_read(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoPermissionCheckerRef,
    ) -> Result<(), AuthorizationError> {
        self.check_repo_metadata_read(ctx, repo)
            .await
            .permitted_or_else(|| self.permission_denied(ctx, DeniedAction::RepoMetadataRead))
    }

    pub async fn check_path_read(
        &self,
        ctx: &CoreContext,
        repo: &(impl RepoPermissionCheckerRef + AclRegionsRef),
        csid: ChangesetId,
        path: Option<&MPath>,
    ) -> Result<AuthorizationCheckOutcome> {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::Identity
            | AuthorizationContext::ReadOnlyIdentity
            | AuthorizationContext::DraftOnlyIdentity
            | AuthorizationContext::Service(_) => {
                // Check the caller's identity permits read access.  Acting as
                // a service does not change read access, so we check the
                // identity in this case also.
                repo.repo_permission_checker()
                    .check_if_read_access_allowed(ctx.metadata().identities())
                    .await
                    || {
                        let rules = repo.acl_regions().associated_rules(ctx, csid, path).await?;
                        let acls = rules.hipster_acls();
                        repo.repo_permission_checker()
                            .check_if_region_read_access_allowed(&acls, ctx.metadata().identities())
                            .await
                    }
            }
        };
        Ok(AuthorizationCheckOutcome::from_permitted(permitted))
    }

    pub async fn require_path_read(
        &self,
        ctx: &CoreContext,
        repo: &(impl RepoPermissionCheckerRef + AclRegionsRef),
        csid: ChangesetId,
        path: Option<&MPath>,
    ) -> Result<(), AuthorizationError> {
        self.check_path_read(ctx, repo, csid, path)
            .await?
            .permitted_or_else(|| {
                self.permission_denied(ctx, DeniedAction::PathRead(csid, path.cloned()))
            })
    }

    /// Check whether the user has general draft access to the repo.
    ///
    /// This does not check specific paths or bookmarks, which must be checked
    /// separately.
    pub async fn check_full_repo_draft(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoPermissionCheckerRef,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::Identity | AuthorizationContext::DraftOnlyIdentity => {
                repo.repo_permission_checker()
                    .check_if_draft_access_allowed_with_tunable_enforcement(
                        ctx,
                        ctx.metadata().identities(),
                    )
                    .await
            }
            // The services have narrowly defined permissions. Never full-repo.
            AuthorizationContext::Service(..) | AuthorizationContext::ReadOnlyIdentity => false,
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that the user has general draft access to the repo, and return
    /// and error if they do not.
    ///
    /// This does not check specific paths or bookmarks, which must be checked
    /// separately.
    pub async fn require_full_repo_draft(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoPermissionCheckerRef,
    ) -> Result<(), AuthorizationError> {
        self.check_full_repo_draft(ctx, repo)
            .await
            .permitted_or_else(|| self.permission_denied(ctx, DeniedAction::FullRepoDraft))
    }

    /// Check whether the user has general write access to the repo.
    ///
    /// This does not check specific paths or bookmarks, which must be checked
    /// separately.
    ///
    /// In cases where write operation covers draft data the draft access will
    /// be used.
    pub async fn check_repo_write(
        &self,
        ctx: &CoreContext,
        repo: &(impl RepoPermissionCheckerRef + RepoConfigRef),
        op: RepoWriteOperation,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::DraftOnlyIdentity => {
                if op.is_draft() {
                    repo.repo_permission_checker()
                        .check_if_draft_access_allowed_with_tunable_enforcement(
                            ctx,
                            ctx.metadata().identities(),
                        )
                        .await
                } else {
                    false
                }
            }
            AuthorizationContext::Identity => {
                if op.is_draft() {
                    repo.repo_permission_checker()
                        .check_if_draft_access_allowed_with_tunable_enforcement(
                            ctx,
                            ctx.metadata().identities(),
                        )
                        .await
                } else {
                    repo.repo_permission_checker()
                        .check_if_write_access_allowed(ctx.metadata().identities())
                        .await
                }
            }
            AuthorizationContext::Service(service_name) => {
                // Check the caller is permitted to act as this service.
                repo
                    .repo_permission_checker()
                    .check_if_service_writes_allowed(ctx.metadata().identities(), service_name)
                    .await &&
                // Check the service is allowed to perform this operation
                repo
                    .repo_config()
                    .source_control_service
                    .service_write_method_permitted(service_name, op.method_name())
            }
            AuthorizationContext::ReadOnlyIdentity => false,
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that the user has general write access to the repo, and return
    /// and error if they do not.
    ///
    /// This does not check specific paths or bookmarks, which must be checked
    /// separately.
    pub async fn require_repo_write(
        &self,
        ctx: &CoreContext,
        repo: &(impl RepoPermissionCheckerRef + RepoConfigRef),
        op: RepoWriteOperation,
    ) -> Result<(), AuthorizationError> {
        self.check_repo_write(ctx, repo, op)
            .await
            .permitted_or_else(|| self.permission_denied(ctx, DeniedAction::RepoWrite(op)))
    }

    /// Check whether a user with write permissions may write to any path in the repo.
    pub async fn check_any_path_write(
        &self,
        _ctx: &CoreContext,
        repo: &impl RepoConfigRef,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess | AuthorizationContext::Identity => true,
            AuthorizationContext::Service(service_name) => repo
                .repo_config()
                .source_control_service
                .service_write_all_paths_permitted(service_name),
            AuthorizationContext::ReadOnlyIdentity | AuthorizationContext::DraftOnlyIdentity => {
                false
            }
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that a user with write permissions may write to the paths in
    /// a changeset (i.e., is the user permitted to land this changeset, based
    /// on the paths that it touches).
    pub async fn require_changeset_paths_write(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoConfigRef,
        changeset: &BonsaiChangeset,
    ) -> Result<(), AuthorizationError> {
        match self {
            AuthorizationContext::FullAccess | AuthorizationContext::Identity => Ok(()),
            AuthorizationContext::Service(service_name) => repo
                .repo_config()
                .source_control_service
                .service_write_paths_permitted(service_name, changeset)
                .map_err(|path| self.permission_denied(ctx, DeniedAction::PathWrite(path.clone()))),
            AuthorizationContext::ReadOnlyIdentity | AuthorizationContext::DraftOnlyIdentity => {
                Err(self.permission_denied(
                    ctx,
                    DeniedAction::PathWrite(
                        changeset
                            .file_changes_map()
                            .keys()
                            .next()
                            .cloned()
                            .ok_or_else(|| anyhow!("no writes allowed in readonly mode!"))?,
                    ),
                ))
            }
        }
    }

    /// Check whether the user is allowed to modify (create, update or delete)
    /// a particular bookmark.
    pub async fn check_bookmark_modify(
        &self,
        ctx: &CoreContext,
        repo: &(impl RepoConfigRef + RepoBookmarkAttrsRef),
        bookmark: &BookmarkKey,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::Identity | AuthorizationContext::DraftOnlyIdentity => {
                let user = ctx.metadata().unix_name().unwrap_or("svcscm");
                repo.repo_bookmark_attrs()
                    .is_allowed_user(ctx, user, bookmark)
                    .await

                // TODO: Check using ctx.identities, and deny if neither are provided.
            }
            AuthorizationContext::Service(service_name) => {
                // Check this service is permitted to modify this bookmark.
                repo.repo_config()
                    .source_control_service
                    .service_write_bookmark_permitted(service_name, bookmark)
            }
            AuthorizationContext::ReadOnlyIdentity => false,
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that the user is allowed to modify (create, update or delete)
    /// a particular bookmark.
    pub async fn require_bookmark_modify(
        &self,
        ctx: &CoreContext,
        repo: &(impl RepoConfigRef + RepoBookmarkAttrsRef),
        bookmark: &BookmarkKey,
    ) -> Result<(), AuthorizationError> {
        self.check_bookmark_modify(ctx, repo, bookmark)
            .await
            .permitted_or_else(|| {
                self.permission_denied(ctx, DeniedAction::BookmarkModification(bookmark.clone()))
            })
    }

    /// Check whether the user is allowed to set the Git mapping for a
    /// changeset to a commit that we cannot prove is round-trippable for
    /// the given Git commit id.
    pub async fn check_override_git_mapping(
        &self,
        _ctx: &CoreContext,
        repo: &impl RepoConfigRef,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::Identity => {
                // Users are never allowed to do this.
                false
            }
            AuthorizationContext::Service(service_name) => {
                // Services are allowed to do this if they are configured to
                // allow the method.
                repo.repo_config()
                    .source_control_service
                    .service_write_method_permitted(service_name, "set_git_mapping_from_changeset")
            }
            AuthorizationContext::ReadOnlyIdentity | AuthorizationContext::DraftOnlyIdentity => {
                false
            }
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that the user is allowed to set the Git mapping for a
    /// changeset to a commit that we cannot prove is round-trippable for
    /// the given Git commit id.
    pub async fn require_override_git_mapping(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoConfigRef,
    ) -> Result<(), AuthorizationError> {
        self.check_override_git_mapping(ctx, repo)
            .await
            .permitted_or_else(|| self.permission_denied(ctx, DeniedAction::OverrideGitMapping))
    }

    /// Check whether the caller is allowed to invoke git-import related
    /// operations for the given repo.
    pub async fn check_git_import_operations(
        &self,
        _ctx: &CoreContext,
        repo: &impl RepoConfigRef,
    ) -> AuthorizationCheckOutcome {
        let permitted = match self {
            AuthorizationContext::FullAccess => true,
            AuthorizationContext::Identity => {
                // Users are never allowed to do this.
                false
            }
            AuthorizationContext::Service(service_name) => {
                // Services are allowed to do this if they are configured to
                // allow the method.
                repo.repo_config()
                    .source_control_service
                    .service_write_method_permitted(service_name, GIT_IMPORT_SVC_WRITE_METHOD)
            }
            AuthorizationContext::ReadOnlyIdentity | AuthorizationContext::DraftOnlyIdentity => {
                false
            }
        };
        AuthorizationCheckOutcome::from_permitted(permitted)
    }

    /// Require that the caller is allowed to invoke git-import related
    /// operations for the given repo.
    pub async fn require_git_import_operations(
        &self,
        ctx: &CoreContext,
        repo: &impl RepoConfigRef,
    ) -> Result<(), AuthorizationError> {
        self.check_git_import_operations(ctx, repo)
            .await
            .permitted_or_else(|| self.permission_denied(ctx, DeniedAction::GitImportOperation))
    }
}

/// Write operations that can be performed on a repo.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum RepoWriteOperation {
    /// Create a new (draft) changeset.
    CreateChangeset,

    /// Create a bookmark.
    CreateBookmark(BookmarkKind),

    /// Update a bookmark.
    UpdateBookmark(BookmarkKind),

    /// Delete a bookmark.
    DeleteBookmark(BookmarkKind),

    /// Land a stack to a bookmark (pushrebase and move bookmark)
    LandStack(BookmarkKind),

    /// Perform a megarepo sync
    MegarepoSync,
}

impl RepoWriteOperation {
    /// Returns true if this is an operation that only affects draft commits.
    fn is_draft(&self) -> bool {
        match self {
            RepoWriteOperation::CreateChangeset => true,
            RepoWriteOperation::CreateBookmark(kind)
            | RepoWriteOperation::UpdateBookmark(kind)
            | RepoWriteOperation::DeleteBookmark(kind)
            | RepoWriteOperation::LandStack(kind) => *kind == BookmarkKind::Scratch,
            RepoWriteOperation::MegarepoSync => false,
        }
    }

    /// Returns the name of the operation's method in the source control
    /// service write restrictions config.
    fn method_name(&self) -> &'static str {
        match self {
            RepoWriteOperation::CreateChangeset => "create_changeset",
            RepoWriteOperation::CreateBookmark(_) => "create_bookmark",
            RepoWriteOperation::UpdateBookmark(_) => "move_bookmark",
            RepoWriteOperation::DeleteBookmark(_) => "delete_bookmark",
            RepoWriteOperation::LandStack(_) => "land_stack",
            RepoWriteOperation::MegarepoSync => "megarepo_sync",
        }
    }
}

/// Outcome of an authorization check.
///
/// This enum is returned as the result of an authorization check, and must not
/// be ignored.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[must_use = "outcomes of authorization checks must not be ignored"]
pub enum AuthorizationCheckOutcome {
    Denied,
    Permitted,
}

impl AuthorizationCheckOutcome {
    fn from_permitted(permitted: bool) -> Self {
        if permitted {
            AuthorizationCheckOutcome::Permitted
        } else {
            AuthorizationCheckOutcome::Denied
        }
    }

    pub fn is_permitted(self) -> bool {
        self == AuthorizationCheckOutcome::Permitted
    }

    pub fn is_denied(self) -> bool {
        self == AuthorizationCheckOutcome::Denied
    }

    /// Return an error if the outcome of the check was that it is not permitted.
    pub fn permitted_or_else(
        self,
        err: impl Fn() -> AuthorizationError,
    ) -> Result<(), AuthorizationError> {
        match self {
            AuthorizationCheckOutcome::Permitted => Ok(()),
            AuthorizationCheckOutcome::Denied => Err(err()),
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fmt;

use anyhow::Error;
use bookmarks::BookmarkKey;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use permission_checker::MononokeIdentitySet;
use thiserror::Error;

use crate::context::AuthorizationContext;
use crate::context::RepoWriteOperation;

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum DeniedAction {
    FullRepoRead,
    FullRepoDraft,
    RepoMetadataRead,
    PathRead(ChangesetId, Option<MPath>),
    RepoWrite(RepoWriteOperation),
    PathWrite(MPath),
    BookmarkModification(BookmarkKey),
    OverrideGitMapping,
    GitImportOperation,
}

impl fmt::Display for DeniedAction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DeniedAction::FullRepoRead => f.write_str("Full repo read access"),
            DeniedAction::FullRepoDraft => f.write_str("Full repo draft access"),
            DeniedAction::RepoMetadataRead => f.write_str("Repo metadata read access"),
            DeniedAction::PathRead(csid, None) => {
                write!(f, "Repo read access for root of changeset {}", csid)
            }
            DeniedAction::PathRead(csid, Some(path)) => write!(
                f,
                "Repo read access for path '{}' in changeset {}",
                path, csid
            ),
            DeniedAction::RepoWrite(op) => write!(f, "Repo write access for {:?}", op),
            DeniedAction::PathWrite(path) => write!(f, "Repo write access to path '{}'", path),
            DeniedAction::BookmarkModification(bookmark) => {
                write!(f, "Modification of bookmark '{}'", bookmark)
            }
            DeniedAction::OverrideGitMapping => f.write_str("Overriding of Git mapping"),
            DeniedAction::GitImportOperation => {
                f.write_str("Access for Git-import related operations")
            }
        }
    }
}

#[derive(Debug, Clone)]
pub struct PermissionDenied {
    pub(crate) denied_action: DeniedAction,
    pub(crate) context: AuthorizationContext,
    pub(crate) identities: MononokeIdentitySet,
}

impl fmt::Display for PermissionDenied {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} is not permitted with {:?} for [",
            self.denied_action, self.context
        )?;
        let mut delim = "";
        for id in self.identities.iter() {
            write!(f, "{}{}", delim, id)?;
            delim = ", ";
        }
        f.write_str("]")
    }
}

impl std::error::Error for PermissionDenied {}

#[derive(Debug, Error)]
pub enum AuthorizationError {
    #[error(transparent)]
    PermissionDenied(#[from] PermissionDenied),

    #[error(transparent)]
    Error(#[from] Error),
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

mod context;
mod error;
#[cfg(test)]
mod tests;

pub use crate::context::AuthorizationContext;
pub use crate::context::RepoWriteOperation;
pub use crate::error::AuthorizationError;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkKind;
use context::CoreContext;
use context::SessionContainer;
use fbinit::FacebookInit;
use futures::FutureExt;
use maplit::hashmap;
use maplit::hashset;
use metaconfig_types::RepoConfig;
use metaconfig_types::ServiceWriteRestrictions;
use mononoke_types::PrefixTrie;
use permission_checker::MononokeIdentitySet;
use repo_bookmark_attrs::RepoBookmarkAttrs;
use repo_permission_checker::RepoPermissionChecker;
use tunables::with_tunables_async;
use tunables::MononokeTunables;

use crate::AuthorizationContext;
use crate::RepoWriteOperation;

#[facet::container]
struct Repo {
    #[facet]
    repo_config: RepoConfig,

    #[facet]
    repo_permission_checker: dyn RepoPermissionChecker,

    #[facet]
    repo_bookmark_attrs: RepoBookmarkAttrs,
}

#[fbinit::test]
async fn test_full_access(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo: Repo = test_repo_factory::build_empty(ctx.fb).await?;
    let authz = AuthorizationContext::new_bypass_access_control();

    authz.require_full_repo_read(&ctx, &repo).await?;
    authz
        .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
        .await?;
    authz
        .require_repo_write(
            &ctx,
            &repo,
            RepoWriteOperation::CreateBookmark(BookmarkKind::Scratch),
        )
        .await?;
    authz
        .require_repo_write(
            &ctx,
            &repo,
            RepoWriteOperation::LandStack(BookmarkKind::Publishing),
        )
        .await?;
    authz
        .require_bookmark_modify(&ctx, &repo, &BookmarkKey::new("main")?)
        .await?;

    Ok(())
}

#[derive(Clone, Default, Debug)]
struct TestPermissionChecker {
    read: bool,
    draft: bool,
    write: bool,
    read_only_bypass: bool,
    any_region_read: bool,
    service_writes: HashMap<String, bool>,
}

#[async_trait]
impl RepoPermissionChecker for TestPermissionChecker {
    async fn check_if_read_access_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        self.read
    }

    async fn check_if_any_region_read_access_allowed(
        &self,
        _identities: &MononokeIdentitySet,
    ) -> bool {
        self.any_region_read
    }

    async fn check_if_region_read_access_allowed<'a>(
        &'a self,
        _acls: &'a [&'a str],
        _identities: &'a MononokeIdentitySet,
    ) -> bool {
        false
    }

    async fn check_if_draft_access_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        self.draft
    }

    async fn check_if_write_access_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        self.write
    }

    async fn check_if_read_only_bypass_allowed(&self, _identities: &MononokeIdentitySet) -> bool {
        self.read_only_bypass
    }

    async fn check_if_service_writes_allowed(
        &self,
        _identities: &MononokeIdentitySet,
        service_name: &str,
    ) -> bool {
        self.service_writes
            .get(service_name)
            .copied()
            .unwrap_or(false)
    }
}

#[fbinit::test]
async fn test_user_no_write_access(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let checker = Arc::new(TestPermissionChecker {
        read: true,
        draft: true,
        write: false,
        ..Default::default()
    });
    let repo: Repo = test_repo_factory::TestRepoFactory::new(fb)?
        .with_permission_checker(checker)
        .build()
        .await?;
    let authz = AuthorizationContext::new(&ctx);

    authz.require_full_repo_read(&ctx, &repo).await?;
    authz.require_full_repo_draft(&ctx, &repo).await?;
    authz
        .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
        .await?;
    authz
        .require_repo_write(
            &ctx,
            &repo,
            RepoWriteOperation::CreateBookmark(BookmarkKind::Scratch),
        )
        .await?;
    assert!(
        authz
            .require_repo_write(
                &ctx,
                &repo,
                RepoWriteOperation::LandStack(BookmarkKind::Publishing),
            )
            .await
            .is_err()
    );
    authz
        .require_bookmark_modify(&ctx, &repo, &BookmarkKey::new("main")?)
        .await?;

    Ok(())
}

#[fbinit::test]
async fn test_user_no_draft_enforceent_off(fb: FacebookInit) -> Result<()> {
    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {
        "log_draft_acl_failures".to_string() => true,
        "enforce_draft_acl".to_string() => false,
    });
    with_tunables_async(
        tunables,
        async {
            let ctx = CoreContext::test_mock(fb);
            let checker = Arc::new(TestPermissionChecker {
                read: true,
                draft: false,
                write: false,
                ..Default::default()
            });
            let repo: Repo = test_repo_factory::TestRepoFactory::new(fb)?
                .with_permission_checker(checker)
                .build()
                .await?;
            let authz = AuthorizationContext::new(&ctx);

            authz.require_full_repo_read(&ctx, &repo).await?;
            authz.require_full_repo_draft(&ctx, &repo).await?;
            authz
                .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
                .await?;
            authz
                .require_repo_write(
                    &ctx,
                    &repo,
                    RepoWriteOperation::CreateBookmark(BookmarkKind::Scratch),
                )
                .await?;
            assert!(
                authz
                    .require_repo_write(
                        &ctx,
                        &repo,
                        RepoWriteOperation::LandStack(BookmarkKind::Publishing),
                    )
                    .await
                    .is_err()
            );

            Ok(())
        }
        .boxed(),
    )
    .await
}

#[fbinit::test]
async fn test_user_no_draft_no_write_access(fb: FacebookInit) -> Result<()> {
    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {
        "log_draft_acl_failures".to_string() => true,
        "enforce_draft_acl".to_string() => true,
    });
    with_tunables_async(
        tunables,
        async {
            let ctx = CoreContext::test_mock(fb);
            let checker = Arc::new(TestPermissionChecker {
                read: true,
                draft: false,
                write: false,
                ..Default::default()
            });
            let repo: Repo = test_repo_factory::TestRepoFactory::new(fb)?
                .with_permission_checker(checker)
                .build()
                .await?;
            let authz = AuthorizationContext::new(&ctx);

            authz.require_full_repo_read(&ctx, &repo).await?;
            assert!(authz.require_full_repo_draft(&ctx, &repo).await.is_err());
            assert!(
                authz
                    .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
                    .await
                    .is_err(),
            );
            assert!(
                authz
                    .require_repo_write(
                        &ctx,
                        &repo,
                        RepoWriteOperation::CreateBookmark(BookmarkKind::Scratch),
                    )
                    .await
                    .is_err(),
            );
            assert!(
                authz
                    .require_repo_write(
                        &ctx,
                        &repo,
                        RepoWriteOperation::LandStack(BookmarkKind::Publishing),
                    )
                    .await
                    .is_err()
            );

            // TODO(mitrandir): This seems fishy: why would bookmark_modify succeed when
            // the user has no write acceess!? Even if that's intended this API might be
            // easily misused. We need to audit this.
            authz
                .require_bookmark_modify(&ctx, &repo, &BookmarkKey::new("main")?)
                .await?;

            Ok(())
        }
        .boxed(),
    )
    .await
}

// Write access should give implied draft access. This will help with migration
// to draft access enforcement and allow us to avoid unncecessary duplication of
// ACLs.
#[fbinit::test]
async fn test_user_write_no_draft_access(fb: FacebookInit) -> Result<()> {
    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {
        "log_draft_acl_failures".to_string() => true,
        "enforce_draft_acl".to_string() => true,
    });
    with_tunables_async(
        tunables,
        async {
            let ctx = CoreContext::test_mock(fb);
            let checker = Arc::new(TestPermissionChecker {
                read: true,
                draft: false,
                write: true,
                ..Default::default()
            });
            let repo: Repo = test_repo_factory::TestRepoFactory::new(fb)?
                .with_permission_checker(checker)
                .build()
                .await?;
            let authz = AuthorizationContext::new(&ctx);

            authz.require_full_repo_read(&ctx, &repo).await?;
            authz.require_full_repo_draft(&ctx, &repo).await?;
            authz
                .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
                .await?;
            authz
                .require_repo_write(
                    &ctx,
                    &repo,
                    RepoWriteOperation::CreateBookmark(BookmarkKind::Scratch),
                )
                .await?;
            authz
                .require_repo_write(
                    &ctx,
                    &repo,
                    RepoWriteOperation::LandStack(BookmarkKind::Publishing),
                )
                .await?;
            authz
                .require_bookmark_modify(&ctx, &repo, &BookmarkKey::new("main")?)
                .await?;

            Ok(())
        }
        .boxed(),
    )
    .await
}

#[fbinit::test]
async fn test_service_access(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let checker = Arc::new(TestPermissionChecker {
        service_writes: hashmap! { String::from("test") => true },
        ..Default::default()
    });
    let repo: Repo = test_repo_factory::TestRepoFactory::new(fb)?
        .with_permission_checker(checker)
        .with_config_override(|config| {
            config.source_control_service.service_write_restrictions = hashmap! {
                String::from("test") =>
                ServiceWriteRestrictions {
                    permitted_methods: hashset! { String::from("create_changeset") },
                    permitted_bookmarks: hashset! { String::from("main") },
                    permitted_path_prefixes: PrefixTrie::Included,
                    ..Default::default()
                }
            };
        })
        .build()
        .await?;
    let authz_test_service = AuthorizationContext::new_for_service_writes("test");
    let authz_other_service = AuthorizationContext::new_for_service_writes("other");

    authz_test_service
        .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
        .await?;
    assert!(
        authz_other_service
            .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
            .await
            .is_err()
    );

    // Test service is permitted to modify main.
    authz_test_service
        .require_bookmark_modify(&ctx, &repo, &BookmarkKey::new("main")?)
        .await?;

    // Another service is not permitted to modify main.
    assert!(
        authz_other_service
            .require_bookmark_modify(&ctx, &repo, &BookmarkKey::new("main")?)
            .await
            .is_err()
    );

    Ok(())
}

#[fbinit::test]
async fn test_user_readonly_instance(fb: FacebookInit) -> () {
    let ctx_session = SessionContainer::builder(fb).readonly(true).build();
    let ctx = CoreContext::test_mock_session(ctx_session);
    let checker = Arc::new(TestPermissionChecker {
        read: true,
        draft: true,
        write: true,
        ..Default::default()
    });
    let repo: Repo = test_repo_factory::TestRepoFactory::new(fb)
        .unwrap()
        .with_permission_checker(checker)
        .build()
        .await
        .unwrap();
    let authz = AuthorizationContext::new(&ctx);

    authz.require_full_repo_read(&ctx, &repo).await.unwrap();
    assert!(authz.require_full_repo_draft(&ctx, &repo).await.is_err());
    assert!(
        authz
            .require_repo_write(&ctx, &repo, RepoWriteOperation::CreateChangeset)
            .await
            .is_err(),
    );
    assert!(
        authz
            .require_repo_write(
                &ctx,
                &repo,
                RepoWriteOperation::CreateBookmark(BookmarkKind::Scratch),
            )
            .await
            .is_err(),
    );
    assert!(
        authz
            .require_repo_write(
                &ctx,
                &repo,
                RepoWriteOperation::LandStack(BookmarkKind::Publishing),
            )
            .await
            .is_err()
    );
    assert!(
        authz
            .require_bookmark_modify(&ctx, &repo, &BookmarkKey::new("main").unwrap())
            .await
            .is_err()
    );
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use filestore::FetchKey;
use mercurial_types::HgChangesetId;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ErrorKind {
    #[error("Bonsai not found for hg changeset: {0:?}")]
    BonsaiNotFoundForHgChangeset(HgChangesetId),
    #[error("missing content {0:?}")]
    MissingContent(FetchKey),
}