use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Surface {
Local,
Tailnet,
}
impl Surface {
pub const fn prefix(self) -> &'static str {
match self {
Self::Local => "tailscale_",
Self::Tailnet => "tailnet_",
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Local => "local",
Self::Tailnet => "tailnet",
}
}
}
impl fmt::Display for Surface {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub const TAILNET_VERBS: &[&str] = &[
"accept",
"approve",
"authorize",
"create",
"delete",
"disable",
"enable",
"expire",
"get",
"list",
"preview",
"rename",
"replace",
"resend",
"restore",
"rotate",
"set",
"suspend",
"test",
"update",
"validate",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Tier {
Read,
Write,
Destructive,
}
impl Tier {
pub const fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
Self::Destructive => "destructive",
}
}
pub const fn flag(self) -> Option<&'static str> {
match self {
Self::Read => None,
Self::Write => Some("--allow-write"),
Self::Destructive => Some("--allow-destructive"),
}
}
}
impl fmt::Display for Tier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Toolset {
LocalStatus,
LocalPrefs,
LocalServe,
LocalFiles,
LocalLock,
LocalDebug,
LocalPassthrough,
TailnetDevices,
TailnetInvites,
TailnetLogging,
TailnetDns,
TailnetKeys,
TailnetPolicy,
TailnetPosture,
TailnetUsers,
TailnetSettings,
TailnetWebhooks,
TailnetServices,
TailnetOauthApps,
TailnetOrg,
}
impl Toolset {
pub const ALL: &'static [Toolset] = &[
Self::LocalStatus,
Self::LocalPrefs,
Self::LocalServe,
Self::LocalFiles,
Self::LocalLock,
Self::LocalDebug,
Self::LocalPassthrough,
Self::TailnetDevices,
Self::TailnetInvites,
Self::TailnetLogging,
Self::TailnetDns,
Self::TailnetKeys,
Self::TailnetPolicy,
Self::TailnetPosture,
Self::TailnetUsers,
Self::TailnetSettings,
Self::TailnetWebhooks,
Self::TailnetServices,
Self::TailnetOauthApps,
Self::TailnetOrg,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::LocalStatus => "local-status",
Self::LocalPrefs => "local-prefs",
Self::LocalServe => "local-serve",
Self::LocalFiles => "local-files",
Self::LocalLock => "local-lock",
Self::LocalDebug => "local-debug",
Self::LocalPassthrough => "local-passthrough",
Self::TailnetDevices => "tailnet-devices",
Self::TailnetInvites => "tailnet-invites",
Self::TailnetLogging => "tailnet-logging",
Self::TailnetDns => "tailnet-dns",
Self::TailnetKeys => "tailnet-keys",
Self::TailnetPolicy => "tailnet-policy",
Self::TailnetPosture => "tailnet-posture",
Self::TailnetUsers => "tailnet-users",
Self::TailnetSettings => "tailnet-settings",
Self::TailnetWebhooks => "tailnet-webhooks",
Self::TailnetServices => "tailnet-services",
Self::TailnetOauthApps => "tailnet-oauth-apps",
Self::TailnetOrg => "tailnet-org",
}
}
pub const fn surface(self) -> Surface {
match self {
Self::LocalStatus
| Self::LocalPrefs
| Self::LocalServe
| Self::LocalFiles
| Self::LocalLock
| Self::LocalDebug
| Self::LocalPassthrough => Surface::Local,
_ => Surface::Tailnet,
}
}
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|t| t.as_str() == s)
}
#[cfg(test)]
fn all_is_exhaustive() {}
}
impl fmt::Display for Toolset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Annotations {
pub read_only: bool,
pub destructive: bool,
pub idempotent: bool,
pub open_world: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct ToolMeta {
pub name: &'static str,
pub toolset: Toolset,
pub tier: Tier,
pub summary: &'static str,
pub self_severing: bool,
pub severs_local_node: bool,
pub requires_confirmation: bool,
pub idempotent: bool,
pub varying_tier: bool,
pub min_version: Option<&'static str>,
pub platforms: Option<&'static [&'static str]>,
}
impl ToolMeta {
pub const fn surface(&self) -> Surface {
self.toolset.surface()
}
pub fn runs_here(&self) -> bool {
self.platforms
.is_none_or(|allowed| allowed.contains(&std::env::consts::OS))
}
pub const fn annotations(&self) -> Annotations {
Annotations {
read_only: !self.varying_tier && matches!(self.tier, Tier::Read),
destructive: self.varying_tier || matches!(self.tier, Tier::Destructive),
idempotent: self.idempotent,
open_world: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toolset_all_covers_every_variant() {
let mut names: Vec<&str> = Toolset::ALL.iter().map(|t| t.as_str()).collect();
names.sort_unstable();
let before = names.len();
names.dedup();
assert_eq!(before, names.len(), "duplicate toolset name");
for t in Toolset::ALL {
assert_eq!(Toolset::parse(t.as_str()), Some(*t));
}
Toolset::all_is_exhaustive();
}
#[test]
fn toolset_names_are_prefixed_by_surface() {
for t in Toolset::ALL {
let expected = match t.surface() {
Surface::Local => "local-",
Surface::Tailnet => "tailnet-",
};
assert!(
t.as_str().starts_with(expected),
"{t} does not carry its surface prefix"
);
}
}
#[test]
fn tiers_order_from_least_to_most_dangerous() {
assert!(Tier::Read < Tier::Write);
assert!(Tier::Write < Tier::Destructive);
}
#[test]
fn annotations_follow_the_tier() {
let read = ToolMeta {
name: "tailscale_status",
toolset: Toolset::LocalStatus,
tier: Tier::Read,
summary: "",
self_severing: false,
severs_local_node: false,
requires_confirmation: false,
idempotent: true,
varying_tier: false,
min_version: None,
platforms: None,
};
assert!(read.annotations().read_only);
assert!(!read.annotations().destructive);
assert!(read.annotations().open_world);
let destructive = ToolMeta {
tier: Tier::Destructive,
..read
};
assert!(!destructive.annotations().read_only);
assert!(destructive.annotations().destructive);
}
#[test]
fn a_varying_tier_is_annotated_at_its_worst_case() {
let passthrough = ToolMeta {
name: "tailscale_run",
toolset: Toolset::LocalPassthrough,
tier: Tier::Read,
summary: "",
self_severing: false,
severs_local_node: false,
requires_confirmation: false,
idempotent: false,
varying_tier: true,
min_version: None,
platforms: None,
};
assert!(!passthrough.annotations().read_only);
assert!(passthrough.annotations().destructive);
}
}