use crate::internal::domain::{ErrorCode, GatewayError};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
pub const HEALTH_READ: &str = "ibkr:health:read";
pub const ACCOUNTS_READ: &str = "ibkr:accounts:read";
pub const PORTFOLIO_READ: &str = "ibkr:portfolio:read";
pub const POSITIONS_READ: &str = "ibkr:positions:read";
pub const MARKETDATA_READ: &str = "ibkr:marketdata:read";
pub const ORDERS_READ: &str = "ibkr:orders:read";
pub const AUDIT_READ: &str = "ibkr:audit:read";
pub const AUDIT_EXPORT: &str = "ibkr:audit:export";
pub const ORDERS_PREVIEW: &str = "ibkr:orders:preview";
pub const RISK_READ: &str = "ibkr:risk:read";
pub const ORDERS_PAPER_SUBMIT: &str = "ibkr:orders:paper:submit";
pub const ORDERS_PAPER_CANCEL: &str = "ibkr:orders:paper:cancel";
pub const ORDERS_PAPER_MODIFY: &str = "ibkr:orders:paper:modify";
pub const ORDERS_LIVE_SUBMIT: &str = "ibkr:orders:live:submit";
pub const ORDERS_LIVE_CANCEL: &str = "ibkr:orders:live:cancel";
pub const ORDERS_LIVE_MODIFY: &str = "ibkr:orders:live:modify";
pub const OPTIONS_READ: &str = "ibkr:options:read";
pub const MARKETDATA_DEPTH_READ: &str = "ibkr:marketdata:depth:read";
pub const SCANNER_READ: &str = "ibkr:scanner:read";
pub const NEWS_READ: &str = "ibkr:news:read";
pub const FUNDAMENTALS_READ: &str = "ibkr:fundamentals:read";
pub const CALENDAR_READ: &str = "ibkr:calendar:read";
pub const CURRENCY_READ: &str = "ibkr:currency:read";
pub const TRANSFERS_READ: &str = "ibkr:transfers:read";
pub const APPROVALS_CREATE: &str = "ibkr:approvals:create";
pub const READ_SCOPES: &[&str] = &[
HEALTH_READ,
ACCOUNTS_READ,
PORTFOLIO_READ,
POSITIONS_READ,
MARKETDATA_READ,
ORDERS_READ,
AUDIT_READ,
AUDIT_EXPORT,
RISK_READ,
OPTIONS_READ,
MARKETDATA_DEPTH_READ,
SCANNER_READ,
NEWS_READ,
FUNDAMENTALS_READ,
CALENDAR_READ,
CURRENCY_READ,
TRANSFERS_READ,
];
pub const PREVIEW_SCOPES: &[&str] = &[ORDERS_PREVIEW, RISK_READ];
pub const PAPER_SCOPES: &[&str] = &[
ORDERS_PAPER_SUBMIT,
ORDERS_PAPER_CANCEL,
ORDERS_PAPER_MODIFY,
APPROVALS_CREATE,
];
pub const LIVE_SCOPES: &[&str] = &[ORDERS_LIVE_SUBMIT, ORDERS_LIVE_CANCEL, ORDERS_LIVE_MODIFY];
pub const LOCAL_SCOPES: &[&str] = &[
HEALTH_READ,
ACCOUNTS_READ,
PORTFOLIO_READ,
POSITIONS_READ,
MARKETDATA_READ,
ORDERS_READ,
AUDIT_READ,
AUDIT_EXPORT,
ORDERS_PREVIEW,
RISK_READ,
ORDERS_PAPER_SUBMIT,
ORDERS_PAPER_CANCEL,
ORDERS_PAPER_MODIFY,
ORDERS_LIVE_SUBMIT,
ORDERS_LIVE_CANCEL,
ORDERS_LIVE_MODIFY,
OPTIONS_READ,
MARKETDATA_DEPTH_READ,
SCANNER_READ,
NEWS_READ,
FUNDAMENTALS_READ,
CALENDAR_READ,
CURRENCY_READ,
TRANSFERS_READ,
APPROVALS_CREATE,
];
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ScopeSet {
scopes: BTreeSet<String>,
}
impl ScopeSet {
pub fn read_only(
scopes: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, GatewayError> {
let scopes = scopes
.into_iter()
.map(Into::into)
.collect::<BTreeSet<String>>();
if let Some(scope) = scopes.iter().find(|scope| !is_read_scope(scope)) {
return Err(GatewayError::new(
ErrorCode::AuthScopeNotAllowedInMvp,
format!("Scope is not allowed in a read-only scope set: {scope}"),
false,
Some("Remove write, remote, sidecar, or live scopes".to_string()),
));
}
Ok(Self { scopes })
}
pub fn local_with_preview(
scopes: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, GatewayError> {
Self::local_matching(scopes, is_preview_scope_set)
}
pub fn local_with_paper(
scopes: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, GatewayError> {
Self::local_matching(scopes, is_paper_scope_set)
}
pub fn local_with_live(
scopes: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, GatewayError> {
Self::local(scopes)
}
fn local(scopes: impl IntoIterator<Item = impl Into<String>>) -> Result<Self, GatewayError> {
Self::local_matching(scopes, is_local_scope)
}
fn local_matching(
scopes: impl IntoIterator<Item = impl Into<String>>,
allowed: fn(&str) -> bool,
) -> Result<Self, GatewayError> {
let scopes = scopes
.into_iter()
.map(Into::into)
.collect::<BTreeSet<String>>();
if let Some(scope) = scopes.iter().find(|scope| !allowed(scope)) {
return Err(GatewayError::new(
ErrorCode::AuthScopeNotAllowedInMvp,
format!("Scope is not allowed locally: {scope}"),
false,
Some("Remove scopes outside the selected local capability tier".to_string()),
));
}
Ok(Self { scopes })
}
#[must_use]
pub fn contains(&self, required_scope: &str) -> bool {
self.scopes.contains(required_scope)
}
#[must_use]
pub const fn as_set(&self) -> &BTreeSet<String> {
&self.scopes
}
}
#[must_use]
pub fn is_read_scope(scope: &str) -> bool {
READ_SCOPES.contains(&scope)
}
#[must_use]
pub fn is_local_scope(scope: &str) -> bool {
LOCAL_SCOPES.contains(&scope)
}
fn is_preview_scope_set(scope: &str) -> bool {
is_read_scope(scope) || PREVIEW_SCOPES.contains(&scope)
}
fn is_paper_scope_set(scope: &str) -> bool {
is_preview_scope_set(scope) || PAPER_SCOPES.contains(&scope)
}
pub fn require_scope(scopes: &ScopeSet, required_scope: &str) -> Result<(), GatewayError> {
if scopes.contains(required_scope) {
Ok(())
} else {
Err(GatewayError::new(
ErrorCode::AuthMissingScope,
format!("Missing required scope: {required_scope}"),
false,
Some("Enable the required local read scope".to_string()),
))
}
}