newton-core 0.4.17

newton protocol core sdk
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

/// API permission levels for access control.
///
/// The `Admin` tier is intentionally read-only despite the name: it grants
/// `RpcRead` AND lifts the per-policy ownership requirement on the two
/// read-with-ownership endpoints (`newt_simulatePolicy`,
/// `newt_simulatePolicyDataWithClient`). It does NOT grant `RpcWrite`.
/// Holders cannot create tasks, store secrets, register webhooks, or call
/// any other state-changing endpoint.
///
/// Why a distinct tier rather than just `RpcRead` + a per-request bypass
/// flag: the bypass increments `gateway_owner_check_bypassed_total{chain_id,
/// endpoint}` and is the only observable signal of privileged read activity.
/// Tying the bypass to a `permissions` row in `api_keys` makes every bypass
/// attributable to a specific key with `permissions @> ARRAY['admin']`. A
/// per-request flag would remove the issuance ceremony (the operator's
/// `--i-understand-admin-bypasses-getowner` acknowledgement) and lose the
/// audit trail. See `crates/gateway/src/rpc/api/common.rs::verify_policy_client_ownership`.
///
/// First-party callers (Newton Dashboard health checks, CI canaries,
/// internal e2e test runners, demo dashboards) are the use case: they
/// legitimately read across policy clients they don't own on-chain.
/// External callers issued an Admin key would gain no write rights from
/// the tier alone.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ApiPermission {
    /// Access to JSON-RPC endpoint (includes both read and write methods)
    Rpc,
    /// Permission to execute read-only RPC methods (newt_simulateTask, queries)
    RpcRead,
    /// Permission to execute state-changing RPC methods (newt_createTask, newt_sendTask)
    RpcWrite,
    /// Read-only tier with on-chain `getOwner()` bypass on read endpoints
    /// that resolve through a `PolicyClient` contract. Despite the name,
    /// `Admin` does NOT imply `RpcWrite` — see the type-level docs above.
    Admin,
}

impl ApiPermission {
    /// Returns all available permissions
    pub fn all() -> HashSet<Self> {
        use ApiPermission::*;
        [Rpc, RpcRead, RpcWrite, Admin].into_iter().collect()
    }

    /// Returns read-only permissions (safe for public access)
    pub fn read_only() -> HashSet<Self> {
        use ApiPermission::*;
        [RpcRead].into_iter().collect()
    }

    /// Checks if this permission implies another permission.
    ///
    /// Implication graph:
    /// - `Admin` implies `RpcRead` (and itself). Crucially, `Admin` does
    ///   NOT imply `RpcWrite` — Admin is a read-only tier with a
    ///   getOwner-bypass capability, not a god-mode key. A previous
    ///   version returned `true` unconditionally, which would let an
    ///   Admin-flagged key pass every `has_permission(&RpcWrite)` gate
    ///   and effectively bypass all write-path authorization. See the
    ///   type-level doc on `ApiPermission` for rationale.
    /// - `Rpc` implies `Rpc` itself, `RpcRead`, and `RpcWrite` (combined
    ///   access). The explicit `Self::Rpc` arm closes a dormant footgun
    ///   where `Rpc.implies(&Rpc)` would have returned false because the
    ///   wildcard `_ => self == other` is unreachable for any arm matched
    ///   above. No production callsite currently checks
    ///   `has_permission(&Rpc)`, but encoding the self-implies contract
    ///   keeps the implication graph internally consistent and resistant
    ///   to future callsite additions.
    /// - All others imply only themselves.
    pub fn implies(&self, other: &Self) -> bool {
        match self {
            Self::Admin => matches!(other, Self::RpcRead | Self::Admin),
            Self::Rpc => matches!(other, Self::Rpc | Self::RpcRead | Self::RpcWrite),
            _ => self == other,
        }
    }
}