Skip to main content

ironflow_store/entities/
api_key_scope.rs

1//! Scopes for API key permissions.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8/// Permission scope for an API key.
9///
10/// Each scope grants access to a specific set of actions.
11/// A key with no scopes has no permissions.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ApiKeyScope {
15    /// Read workflow definitions.
16    WorkflowsRead,
17    /// Read runs and their steps.
18    RunsRead,
19    /// Create new runs (trigger workflows).
20    RunsWrite,
21    /// Cancel, approve, reject, retry runs.
22    RunsManage,
23    /// Read aggregated statistics.
24    StatsRead,
25    /// Full access to all operations.
26    Admin,
27}
28
29impl ApiKeyScope {
30    /// Check whether this scope grants the required permission.
31    pub fn permits(&self, required: &ApiKeyScope) -> bool {
32        match self {
33            ApiKeyScope::Admin => true,
34            other => other == required,
35        }
36    }
37
38    /// Check whether a set of scopes grants the required permission.
39    pub fn has_permission(scopes: &[ApiKeyScope], required: &ApiKeyScope) -> bool {
40        scopes.iter().any(|s| s.permits(required))
41    }
42
43    /// All available scopes (excluding admin).
44    pub fn all_non_admin() -> Vec<ApiKeyScope> {
45        vec![
46            ApiKeyScope::WorkflowsRead,
47            ApiKeyScope::RunsRead,
48            ApiKeyScope::RunsWrite,
49            ApiKeyScope::RunsManage,
50            ApiKeyScope::StatsRead,
51        ]
52    }
53}
54
55impl fmt::Display for ApiKeyScope {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        let s = match self {
58            ApiKeyScope::WorkflowsRead => "workflows_read",
59            ApiKeyScope::RunsRead => "runs_read",
60            ApiKeyScope::RunsWrite => "runs_write",
61            ApiKeyScope::RunsManage => "runs_manage",
62            ApiKeyScope::StatsRead => "stats_read",
63            ApiKeyScope::Admin => "admin",
64        };
65        f.write_str(s)
66    }
67}
68
69/// Error when parsing an invalid scope string.
70#[derive(Debug, Clone)]
71pub struct InvalidScope(pub String);
72
73impl fmt::Display for InvalidScope {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(f, "invalid API key scope: {}", self.0)
76    }
77}
78
79impl std::error::Error for InvalidScope {}
80
81impl FromStr for ApiKeyScope {
82    type Err = InvalidScope;
83
84    fn from_str(s: &str) -> Result<Self, Self::Err> {
85        match s {
86            "workflows_read" => Ok(ApiKeyScope::WorkflowsRead),
87            "runs_read" => Ok(ApiKeyScope::RunsRead),
88            "runs_write" => Ok(ApiKeyScope::RunsWrite),
89            "runs_manage" => Ok(ApiKeyScope::RunsManage),
90            "stats_read" => Ok(ApiKeyScope::StatsRead),
91            "admin" => Ok(ApiKeyScope::Admin),
92            _ => Err(InvalidScope(s.to_string())),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn admin_permits_everything() {
103        let admin = ApiKeyScope::Admin;
104        assert!(admin.permits(&ApiKeyScope::RunsRead));
105        assert!(admin.permits(&ApiKeyScope::RunsWrite));
106        assert!(admin.permits(&ApiKeyScope::RunsManage));
107        assert!(admin.permits(&ApiKeyScope::WorkflowsRead));
108        assert!(admin.permits(&ApiKeyScope::StatsRead));
109        assert!(admin.permits(&ApiKeyScope::Admin));
110    }
111
112    #[test]
113    fn regular_scope_only_permits_itself() {
114        let scope = ApiKeyScope::RunsRead;
115        assert!(scope.permits(&ApiKeyScope::RunsRead));
116        assert!(!scope.permits(&ApiKeyScope::RunsWrite));
117        assert!(!scope.permits(&ApiKeyScope::Admin));
118    }
119
120    #[test]
121    fn has_permission_with_multiple_scopes() {
122        let scopes = vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead];
123        assert!(ApiKeyScope::has_permission(&scopes, &ApiKeyScope::RunsRead));
124        assert!(ApiKeyScope::has_permission(
125            &scopes,
126            &ApiKeyScope::WorkflowsRead
127        ));
128        assert!(!ApiKeyScope::has_permission(
129            &scopes,
130            &ApiKeyScope::RunsWrite
131        ));
132    }
133
134    #[test]
135    fn roundtrip_display_parse() {
136        let scopes = vec![
137            ApiKeyScope::WorkflowsRead,
138            ApiKeyScope::RunsRead,
139            ApiKeyScope::RunsWrite,
140            ApiKeyScope::RunsManage,
141            ApiKeyScope::StatsRead,
142            ApiKeyScope::Admin,
143        ];
144        for scope in scopes {
145            let s = scope.to_string();
146            let parsed: ApiKeyScope = s.parse().expect("should parse");
147            assert_eq!(parsed, scope);
148        }
149    }
150
151    #[test]
152    fn parse_invalid_scope() {
153        let result = "invalid".parse::<ApiKeyScope>();
154        assert!(result.is_err());
155    }
156
157    #[test]
158    fn serde_roundtrip() {
159        let scope = ApiKeyScope::RunsWrite;
160        let json = serde_json::to_string(&scope).expect("serialize");
161        assert_eq!(json, "\"runs_write\"");
162        let parsed: ApiKeyScope = serde_json::from_str(&json).expect("deserialize");
163        assert_eq!(parsed, scope);
164    }
165
166    #[test]
167    fn all_non_admin_excludes_admin() {
168        let scopes = ApiKeyScope::all_non_admin();
169        assert!(!scopes.contains(&ApiKeyScope::Admin));
170        assert_eq!(scopes.len(), 5);
171    }
172}