Something went wrong. Try again.
A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726use std::collections::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]pub struct PermissionInfo { pub key: &'static str, pub name: &'static str, pub description: &'static str, pub category: &'static str,}
/// All permissions in the system.#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]pub enum Permission { #[serde(rename = "lexicons:create")] LexiconsCreate, #[serde(rename = "lexicons:read")] LexiconsRead, #[serde(rename = "lexicons:delete")] LexiconsDelete,
#[serde(rename = "records:read")] RecordsRead, #[serde(rename = "records:delete")] RecordsDelete, #[serde(rename = "records:delete-collection")] RecordsDeleteCollection,
#[serde(rename = "script-variables:create")] ScriptVariablesCreate, #[serde(rename = "script-variables:read")] ScriptVariablesRead, #[serde(rename = "script-variables:delete")] ScriptVariablesDelete,
#[serde(rename = "users:create")] UsersCreate, #[serde(rename = "users:read")] UsersRead, #[serde(rename = "users:update")] UsersUpdate, #[serde(rename = "users:delete")] UsersDelete,
#[serde(rename = "api-keys:create")] ApiKeysCreate, #[serde(rename = "api-keys:read")] ApiKeysRead, #[serde(rename = "api-keys:delete")] ApiKeysDelete,
#[serde(rename = "backfill:create")] BackfillCreate, #[serde(rename = "backfill:read")] BackfillRead,
#[serde(rename = "stats:read")] StatsRead,
#[serde(rename = "events:read")] EventsRead,
#[serde(rename = "labelers:create")] LabelersCreate, #[serde(rename = "labelers:read")] LabelersRead, #[serde(rename = "labelers:delete")] LabelersDelete,
#[serde(rename = "settings:manage")] SettingsManage,
#[serde(rename = "plugins:read")] PluginsRead, #[serde(rename = "plugins:create")] PluginsCreate, #[serde(rename = "plugins:delete")] PluginsDelete,
#[serde(rename = "api-clients:view")] ApiClientsView, #[serde(rename = "api-clients:create")] ApiClientsCreate, #[serde(rename = "api-clients:edit")] ApiClientsEdit, #[serde(rename = "api-clients:delete")] ApiClientsDelete,
#[serde(rename = "dead-letters:read")] DeadLettersRead, #[serde(rename = "dead-letters:manage")] DeadLettersManage,
#[serde(rename = "spaces:create")] SpacesCreate, #[serde(rename = "spaces:read")] SpacesRead, #[serde(rename = "spaces:update")] SpacesUpdate, #[serde(rename = "spaces:delete")] SpacesDelete, #[serde(rename = "spaces:manage-members")] SpacesManageMembers, #[serde(rename = "spaces:manage-invites")] SpacesManageInvites, #[serde(rename = "spaces:manage-records")] SpacesManageRecords, #[serde(rename = "spaces:manage-credentials")] SpacesManageCredentials,}
impl Permission { /// String representation matching the DB values. pub fn as_str(&self) -> &'static str { match self { Self::LexiconsCreate => "lexicons:create", Self::LexiconsRead => "lexicons:read", Self::LexiconsDelete => "lexicons:delete", Self::RecordsRead => "records:read", Self::RecordsDelete => "records:delete", Self::RecordsDeleteCollection => "records:delete-collection", Self::ScriptVariablesCreate => "script-variables:create", Self::ScriptVariablesRead => "script-variables:read", Self::ScriptVariablesDelete => "script-variables:delete", Self::UsersCreate => "users:create", Self::UsersRead => "users:read", Self::UsersUpdate => "users:update", Self::UsersDelete => "users:delete", Self::ApiKeysCreate => "api-keys:create", Self::ApiKeysRead => "api-keys:read", Self::ApiKeysDelete => "api-keys:delete", Self::BackfillCreate => "backfill:create", Self::BackfillRead => "backfill:read", Self::StatsRead => "stats:read", Self::EventsRead => "events:read", Self::LabelersCreate => "labelers:create", Self::LabelersRead => "labelers:read", Self::LabelersDelete => "labelers:delete", Self::SettingsManage => "settings:manage", Self::PluginsRead => "plugins:read", Self::PluginsCreate => "plugins:create", Self::PluginsDelete => "plugins:delete", Self::ApiClientsView => "api-clients:view", Self::ApiClientsCreate => "api-clients:create", Self::ApiClientsEdit => "api-clients:edit", Self::ApiClientsDelete => "api-clients:delete", Self::DeadLettersRead => "dead-letters:read", Self::DeadLettersManage => "dead-letters:manage", Self::SpacesCreate => "spaces:create", Self::SpacesRead => "spaces:read", Self::SpacesUpdate => "spaces:update", Self::SpacesDelete => "spaces:delete", Self::SpacesManageMembers => "spaces:manage-members", Self::SpacesManageInvites => "spaces:manage-invites", Self::SpacesManageRecords => "spaces:manage-records", Self::SpacesManageCredentials => "spaces:manage-credentials", } }
pub fn info(&self) -> PermissionInfo { match self { Self::LexiconsCreate => PermissionInfo { key: "lexicons:create", name: "Create Lexicons", description: "Upload and register new lexicon schemas", category: "Lexicons", }, Self::LexiconsRead => PermissionInfo { key: "lexicons:read", name: "View Lexicons", description: "View registered lexicon schemas", category: "Lexicons", }, Self::LexiconsDelete => PermissionInfo { key: "lexicons:delete", name: "Delete Lexicons", description: "Remove lexicon schemas", category: "Lexicons", }, Self::RecordsRead => PermissionInfo { key: "records:read", name: "View Records", description: "Browse indexed AT Protocol records", category: "Records", }, Self::RecordsDelete => PermissionInfo { key: "records:delete", name: "Delete Records", description: "Delete individual records from the index", category: "Records", }, Self::RecordsDeleteCollection => PermissionInfo { key: "records:delete-collection", name: "Delete Collections", description: "Bulk-delete all records in a collection", category: "Records", }, Self::ScriptVariablesCreate => PermissionInfo { key: "script-variables:create", name: "Create Script Variables", description: "Add or update environment variables for Lua scripts", category: "Script Variables", }, Self::ScriptVariablesRead => PermissionInfo { key: "script-variables:read", name: "View Script Variables", description: "View script environment variable keys and values", category: "Script Variables", }, Self::ScriptVariablesDelete => PermissionInfo { key: "script-variables:delete", name: "Delete Script Variables", description: "Remove script environment variables", category: "Script Variables", }, Self::UsersCreate => PermissionInfo { key: "users:create", name: "Create Users", description: "Add new dashboard users", category: "Users", }, Self::UsersRead => PermissionInfo { key: "users:read", name: "View Users", description: "View the user list and their permissions", category: "Users", }, Self::UsersUpdate => PermissionInfo { key: "users:update", name: "Update Users", description: "Modify user permissions", category: "Users", }, Self::UsersDelete => PermissionInfo { key: "users:delete", name: "Delete Users", description: "Remove dashboard users", category: "Users", }, Self::ApiKeysCreate => PermissionInfo { key: "api-keys:create", name: "Create API Keys", description: "Generate new API keys for admin access", category: "API Keys", }, Self::ApiKeysRead => PermissionInfo { key: "api-keys:read", name: "View API Keys", description: "View existing API keys", category: "API Keys", }, Self::ApiKeysDelete => PermissionInfo { key: "api-keys:delete", name: "Revoke API Keys", description: "Revoke existing API keys", category: "API Keys", }, Self::BackfillCreate => PermissionInfo { key: "backfill:create", name: "Start Backfill", description: "Trigger historical record backfill jobs", category: "Backfill", }, Self::BackfillRead => PermissionInfo { key: "backfill:read", name: "View Backfill", description: "View backfill job status and progress", category: "Backfill", }, Self::StatsRead => PermissionInfo { key: "stats:read", name: "View Stats", description: "View collection statistics and record counts", category: "System", }, Self::EventsRead => PermissionInfo { key: "events:read", name: "View Events", description: "View the event log", category: "System", }, Self::LabelersCreate => PermissionInfo { key: "labelers:create", name: "Add Labelers", description: "Subscribe to external labeler services", category: "Labelers", }, Self::LabelersRead => PermissionInfo { key: "labelers:read", name: "View Labelers", description: "View subscribed labeler services", category: "Labelers", }, Self::LabelersDelete => PermissionInfo { key: "labelers:delete", name: "Remove Labelers", description: "Unsubscribe from labeler services", category: "Labelers", }, Self::SettingsManage => PermissionInfo { key: "settings:manage", name: "Manage Settings", description: "Modify instance settings, logo, and configuration", category: "Settings", }, Self::PluginsRead => PermissionInfo { key: "plugins:read", name: "View Plugins", description: "View installed plugins and their configuration", category: "Plugins", }, Self::PluginsCreate => PermissionInfo { key: "plugins:create", name: "Install Plugins", description: "Install and configure new plugins", category: "Plugins", }, Self::PluginsDelete => PermissionInfo { key: "plugins:delete", name: "Remove Plugins", description: "Uninstall plugins", category: "Plugins", }, Self::ApiClientsView => PermissionInfo { key: "api-clients:view", name: "View API Clients", description: "View registered OAuth API clients", category: "API Clients", }, Self::ApiClientsCreate => PermissionInfo { key: "api-clients:create", name: "Create API Clients", description: "Register new OAuth API clients", category: "API Clients", }, Self::ApiClientsEdit => PermissionInfo { key: "api-clients:edit", name: "Edit API Clients", description: "Modify API client settings and credentials", category: "API Clients", }, Self::ApiClientsDelete => PermissionInfo { key: "api-clients:delete", name: "Delete API Clients", description: "Remove registered API clients", category: "API Clients", }, Self::DeadLettersRead => PermissionInfo { key: "dead-letters:read", name: "View Dead Letters", description: "View failed hook executions", category: "Dead Letters", }, Self::DeadLettersManage => PermissionInfo { key: "dead-letters:manage", name: "Manage Dead Letters", description: "Retry, re-index, or dismiss dead letters", category: "Dead Letters", }, Self::SpacesCreate => PermissionInfo { key: "spaces:create", name: "Create Spaces", description: "Create new permissioned data spaces", category: "Spaces", }, Self::SpacesRead => PermissionInfo { key: "spaces:read", name: "View Spaces", description: "View space details and metadata", category: "Spaces", }, Self::SpacesUpdate => PermissionInfo { key: "spaces:update", name: "Update Spaces", description: "Modify space settings", category: "Spaces", }, Self::SpacesDelete => PermissionInfo { key: "spaces:delete", name: "Delete Spaces", description: "Remove spaces and their data", category: "Spaces", }, Self::SpacesManageMembers => PermissionInfo { key: "spaces:manage-members", name: "Manage Members", description: "Add or remove space members and roles", category: "Spaces", }, Self::SpacesManageInvites => PermissionInfo { key: "spaces:manage-invites", name: "Manage Invites", description: "Create and revoke space invitations", category: "Spaces", }, Self::SpacesManageRecords => PermissionInfo { key: "spaces:manage-records", name: "Manage Records", description: "Read and write records within spaces", category: "Spaces", }, Self::SpacesManageCredentials => PermissionInfo { key: "spaces:manage-credentials", name: "Manage Credentials", description: "Issue and revoke space access credentials", category: "Spaces", }, } }
/// All permissions. pub fn all() -> HashSet<Permission> { HashSet::from([ Self::LexiconsCreate, Self::LexiconsRead, Self::LexiconsDelete, Self::RecordsRead, Self::RecordsDelete, Self::RecordsDeleteCollection, Self::ScriptVariablesCreate, Self::ScriptVariablesRead, Self::ScriptVariablesDelete, Self::UsersCreate, Self::UsersRead, Self::UsersUpdate, Self::UsersDelete, Self::ApiKeysCreate, Self::ApiKeysRead, Self::ApiKeysDelete, Self::BackfillCreate, Self::BackfillRead, Self::StatsRead, Self::EventsRead, Self::LabelersCreate, Self::LabelersRead, Self::LabelersDelete, Self::SettingsManage, Self::PluginsRead, Self::PluginsCreate, Self::PluginsDelete, Self::ApiClientsView, Self::ApiClientsCreate, Self::ApiClientsEdit, Self::ApiClientsDelete, Self::DeadLettersRead, Self::DeadLettersManage, Self::SpacesCreate, Self::SpacesRead, Self::SpacesUpdate, Self::SpacesDelete, Self::SpacesManageMembers, Self::SpacesManageInvites, Self::SpacesManageRecords, Self::SpacesManageCredentials, ]) }}
/// Ordered list of all permissions with metadata.pub fn catalog() -> Vec<PermissionInfo> { use Permission::*; [ LexiconsCreate, LexiconsRead, LexiconsDelete, RecordsRead, RecordsDelete, RecordsDeleteCollection, ScriptVariablesCreate, ScriptVariablesRead, ScriptVariablesDelete, UsersCreate, UsersRead, UsersUpdate, UsersDelete, ApiKeysCreate, ApiKeysRead, ApiKeysDelete, BackfillCreate, BackfillRead, StatsRead, EventsRead, LabelersCreate, LabelersRead, LabelersDelete, SettingsManage, PluginsRead, PluginsCreate, PluginsDelete, ApiClientsView, ApiClientsCreate, ApiClientsEdit, ApiClientsDelete, DeadLettersRead, DeadLettersManage, SpacesCreate, SpacesRead, SpacesUpdate, SpacesDelete, SpacesManageMembers, SpacesManageInvites, SpacesManageRecords, SpacesManageCredentials, ] .iter() .map(|p| p.info()) .collect()}
/// Check whether a permission string is recognized.#[allow(dead_code)]pub fn is_valid(key: &str) -> bool { serde_json::from_value::<Permission>(serde_json::Value::String(key.to_string())).is_ok()}
/// Predefined permission templates.#[derive(Debug, Clone, Copy, Deserialize, Serialize)]#[serde(rename_all = "snake_case")]pub enum Template { Viewer, Operator, Manager, FullAccess,}
#[derive(Serialize)]pub struct TemplateInfo { pub key: String, pub label: &'static str, pub permissions: Vec<&'static str>,}
impl Template { pub const ALL: &[Template] = &[ Template::Viewer, Template::Operator, Template::Manager, Template::FullAccess, ];
pub fn key(&self) -> &'static str { match self { Self::Viewer => "viewer", Self::Operator => "operator", Self::Manager => "manager", Self::FullAccess => "full_access", } }
pub fn label(&self) -> &'static str { match self { Self::Viewer => "Viewer", Self::Operator => "Operator", Self::Manager => "Manager", Self::FullAccess => "Full Access", } }
pub fn info(&self) -> TemplateInfo { TemplateInfo { key: self.key().to_string(), label: self.label(), permissions: self.permissions().iter().map(|p| p.as_str()).collect(), } }
pub fn permissions(&self) -> HashSet<Permission> { match self { Self::Viewer => HashSet::from([ Permission::LexiconsRead, Permission::RecordsRead, Permission::ScriptVariablesRead, Permission::UsersRead, Permission::ApiKeysRead, Permission::BackfillRead, Permission::StatsRead, Permission::EventsRead, Permission::DeadLettersRead, ]), Self::Operator => { let mut perms = Self::Viewer.permissions(); perms.insert(Permission::BackfillCreate); perms.insert(Permission::ApiKeysCreate); perms.insert(Permission::ApiKeysDelete); perms.insert(Permission::DeadLettersManage); perms } Self::Manager => { let mut perms = Self::Operator.permissions(); perms.insert(Permission::LexiconsCreate); perms.insert(Permission::LexiconsDelete); perms.insert(Permission::ScriptVariablesCreate); perms.insert(Permission::ScriptVariablesDelete); perms.insert(Permission::RecordsDelete); perms.insert(Permission::LabelersCreate); perms.insert(Permission::LabelersRead); perms.insert(Permission::LabelersDelete); perms.insert(Permission::SettingsManage); perms.insert(Permission::PluginsRead); perms.insert(Permission::PluginsCreate); perms.insert(Permission::PluginsDelete); perms.insert(Permission::ApiClientsView); perms.insert(Permission::ApiClientsCreate); perms.insert(Permission::ApiClientsEdit); perms.insert(Permission::ApiClientsDelete); perms.insert(Permission::SpacesCreate); perms.insert(Permission::SpacesRead); perms.insert(Permission::SpacesUpdate); perms.insert(Permission::SpacesDelete); perms.insert(Permission::SpacesManageMembers); perms.insert(Permission::SpacesManageInvites); perms.insert(Permission::SpacesManageRecords); perms.insert(Permission::SpacesManageCredentials); perms } Self::FullAccess => Permission::all(), } }}
#[cfg(test)]mod tests { use super::*;
#[test] fn catalog_covers_all_permissions() { let catalog_keys: Vec<&str> = catalog().iter().map(|p| p.key).collect(); for perm in Permission::all() { assert!( catalog_keys.contains(&perm.as_str()), "Permission {} missing from catalog()", perm.as_str() ); } }
#[test] fn catalog_has_no_duplicates() { let entries = catalog(); let mut seen = std::collections::HashSet::new(); for entry in &entries { assert!( seen.insert(entry.key), "Duplicate key in catalog: {}", entry.key ); } }
#[test] fn info_key_matches_as_str() { for perm in Permission::all() { assert_eq!(perm.info().key, perm.as_str()); } }
#[test] fn info_fields_are_nonempty() { for perm in Permission::all() { let info = perm.info(); assert!(!info.name.is_empty(), "{} has empty name", info.key); assert!( !info.description.is_empty(), "{} has empty description", info.key ); assert!(!info.category.is_empty(), "{} has empty category", info.key); } }
#[test] fn is_valid_accepts_known_permissions() { assert!(is_valid("lexicons:create")); assert!(is_valid("spaces:manage-members")); }
#[test] fn is_valid_rejects_unknown_permissions() { assert!(!is_valid("fake:permission")); assert!(!is_valid("")); }
#[test] fn template_full_access_covers_all() { assert_eq!(Template::FullAccess.permissions(), Permission::all()); }
#[test] fn template_viewer_is_subset_of_operator() { let viewer = Template::Viewer.permissions(); let operator = Template::Operator.permissions(); assert!(viewer.is_subset(&operator)); }
#[test] fn template_operator_is_subset_of_manager() { let operator = Template::Operator.permissions(); let manager = Template::Manager.permissions(); assert!(operator.is_subset(&manager)); }
#[test] fn template_info_permissions_match_template_permissions() { for t in Template::ALL { let info = t.info(); let expected: HashSet<&str> = t.permissions().iter().map(|p| p.as_str()).collect(); let actual: HashSet<&str> = info.permissions.into_iter().collect(); assert_eq!(expected, actual, "Template {:?} info mismatch", t); } }
#[test] fn spaces_permissions_are_in_spaces_category() { for entry in catalog() { if entry.key.starts_with("spaces:") { assert_eq!( entry.category, "Spaces", "{} should be in Spaces category", entry.key ); } } }}