Skip to main content

ironflow_store/entities/
api_key_scope.rs

1//! Scopes for API key permissions.
2
3use serde::{Deserialize, Serialize};
4use strum::{Display, EnumString};
5
6/// Permission scope for an API key.
7///
8/// Each scope grants access to a specific set of actions.
9/// A key with no scopes has no permissions.
10#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
12#[serde(rename_all = "snake_case")]
13#[strum(serialize_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    /// Read Provider Accounts and their usage.
26    AccountsRead,
27    /// Create, update, delete and test Provider Accounts.
28    AccountsManage,
29    /// Send signals that resume waiting runs.
30    SignalsSend,
31    /// Full access to all operations.
32    Admin,
33}
34
35impl ApiKeyScope {
36    /// Check whether this scope grants the required permission.
37    pub fn permits(&self, required: &ApiKeyScope) -> bool {
38        match self {
39            ApiKeyScope::Admin => true,
40            other => other == required,
41        }
42    }
43
44    /// Check whether a set of scopes grants the required permission.
45    pub fn has_permission(scopes: &[ApiKeyScope], required: &ApiKeyScope) -> bool {
46        scopes.iter().any(|s| s.permits(required))
47    }
48
49    /// All available scopes (excluding admin).
50    pub fn all_non_admin() -> Vec<ApiKeyScope> {
51        vec![
52            ApiKeyScope::WorkflowsRead,
53            ApiKeyScope::RunsRead,
54            ApiKeyScope::RunsWrite,
55            ApiKeyScope::RunsManage,
56            ApiKeyScope::StatsRead,
57            ApiKeyScope::AccountsRead,
58            ApiKeyScope::AccountsManage,
59            ApiKeyScope::SignalsSend,
60        ]
61    }
62
63    /// Scopes a non-admin member is allowed to use.
64    ///
65    /// Whitelist approach: only these scopes are permitted for members.
66    /// Any scope not listed here is forbidden for non-admin users.
67    pub fn member_allowed() -> &'static [ApiKeyScope] {
68        &[
69            ApiKeyScope::WorkflowsRead,
70            ApiKeyScope::RunsRead,
71            ApiKeyScope::StatsRead,
72        ]
73    }
74
75    /// Check whether all scopes in the set are allowed for a non-admin member.
76    pub fn all_allowed_for_member(scopes: &[ApiKeyScope]) -> bool {
77        let allowed = Self::member_allowed();
78        scopes.iter().all(|s| allowed.contains(s))
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn admin_permits_everything() {
88        let admin = ApiKeyScope::Admin;
89        assert!(admin.permits(&ApiKeyScope::RunsRead));
90        assert!(admin.permits(&ApiKeyScope::RunsWrite));
91        assert!(admin.permits(&ApiKeyScope::RunsManage));
92        assert!(admin.permits(&ApiKeyScope::WorkflowsRead));
93        assert!(admin.permits(&ApiKeyScope::StatsRead));
94        assert!(admin.permits(&ApiKeyScope::Admin));
95    }
96
97    #[test]
98    fn regular_scope_only_permits_itself() {
99        let scope = ApiKeyScope::RunsRead;
100        assert!(scope.permits(&ApiKeyScope::RunsRead));
101        assert!(!scope.permits(&ApiKeyScope::RunsWrite));
102        assert!(!scope.permits(&ApiKeyScope::Admin));
103    }
104
105    #[test]
106    fn has_permission_with_multiple_scopes() {
107        let scopes = vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead];
108        assert!(ApiKeyScope::has_permission(&scopes, &ApiKeyScope::RunsRead));
109        assert!(ApiKeyScope::has_permission(
110            &scopes,
111            &ApiKeyScope::WorkflowsRead
112        ));
113        assert!(!ApiKeyScope::has_permission(
114            &scopes,
115            &ApiKeyScope::RunsWrite
116        ));
117    }
118
119    #[test]
120    fn roundtrip_display_parse() {
121        let scopes = vec![
122            ApiKeyScope::WorkflowsRead,
123            ApiKeyScope::RunsRead,
124            ApiKeyScope::RunsWrite,
125            ApiKeyScope::RunsManage,
126            ApiKeyScope::StatsRead,
127            ApiKeyScope::AccountsRead,
128            ApiKeyScope::AccountsManage,
129            ApiKeyScope::SignalsSend,
130            ApiKeyScope::Admin,
131        ];
132        for scope in scopes {
133            let s = scope.to_string();
134            let parsed: ApiKeyScope = s.parse().expect("should parse");
135            assert_eq!(parsed, scope);
136        }
137    }
138
139    #[test]
140    fn parse_invalid_scope() {
141        let result = "invalid".parse::<ApiKeyScope>();
142        assert!(result.is_err());
143    }
144
145    #[test]
146    fn serde_roundtrip() {
147        let scope = ApiKeyScope::RunsWrite;
148        let json = serde_json::to_string(&scope).expect("serialize");
149        assert_eq!(json, "\"runs_write\"");
150        let parsed: ApiKeyScope = serde_json::from_str(&json).expect("deserialize");
151        assert_eq!(parsed, scope);
152    }
153
154    #[test]
155    fn all_non_admin_excludes_admin() {
156        let scopes = ApiKeyScope::all_non_admin();
157        assert!(!scopes.contains(&ApiKeyScope::Admin));
158        assert_eq!(scopes.len(), 8);
159    }
160
161    #[test]
162    fn member_allowed_is_read_only() {
163        let allowed = ApiKeyScope::member_allowed();
164        assert!(allowed.contains(&ApiKeyScope::WorkflowsRead));
165        assert!(allowed.contains(&ApiKeyScope::RunsRead));
166        assert!(allowed.contains(&ApiKeyScope::StatsRead));
167        assert!(!allowed.contains(&ApiKeyScope::RunsWrite));
168        assert!(!allowed.contains(&ApiKeyScope::RunsManage));
169        assert!(!allowed.contains(&ApiKeyScope::Admin));
170        assert!(!allowed.contains(&ApiKeyScope::AccountsRead));
171        assert!(!allowed.contains(&ApiKeyScope::AccountsManage));
172        assert!(!allowed.contains(&ApiKeyScope::SignalsSend));
173    }
174
175    #[test]
176    fn all_allowed_for_member_accepts_read_scopes() {
177        let scopes = vec![ApiKeyScope::WorkflowsRead, ApiKeyScope::RunsRead];
178        assert!(ApiKeyScope::all_allowed_for_member(&scopes));
179    }
180
181    #[test]
182    fn all_allowed_for_member_rejects_write_scopes() {
183        let scopes = vec![ApiKeyScope::RunsRead, ApiKeyScope::RunsWrite];
184        assert!(!ApiKeyScope::all_allowed_for_member(&scopes));
185    }
186
187    #[test]
188    fn all_allowed_for_member_rejects_admin() {
189        let scopes = vec![ApiKeyScope::Admin];
190        assert!(!ApiKeyScope::all_allowed_for_member(&scopes));
191    }
192}