ironflow_api/
authorization.rs1use ironflow_auth::extractor::{AuthMethod, Authenticated};
9use ironflow_store::entities::ApiKeyScope;
10
11use crate::error::ApiError;
12
13pub trait AuthorizeExt {
42 fn require(&self, scope: ApiKeyScope, admin_only: bool) -> Result<(), ApiError>;
78}
79
80impl AuthorizeExt for Authenticated {
81 fn require(&self, scope: ApiKeyScope, admin_only: bool) -> Result<(), ApiError> {
82 match &self.method {
83 AuthMethod::Jwt { is_admin, .. } => {
84 if admin_only && !is_admin {
85 return Err(ApiError::Forbidden);
86 }
87 }
88 AuthMethod::ApiKey {
89 scopes,
90 owner_is_admin,
91 ..
92 } => {
93 if !ApiKeyScope::has_permission(scopes, &scope) {
94 return Err(ApiError::InsufficientScope);
95 }
96 if admin_only && !owner_is_admin {
97 return Err(ApiError::Forbidden);
98 }
99 }
100 }
101 Ok(())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use uuid::Uuid;
108
109 use super::*;
110
111 fn jwt(is_admin: bool) -> Authenticated {
112 Authenticated {
113 user_id: Uuid::now_v7(),
114 method: AuthMethod::Jwt {
115 username: "user".to_string(),
116 is_admin,
117 },
118 }
119 }
120
121 fn key(scopes: Vec<ApiKeyScope>, owner_is_admin: bool) -> Authenticated {
122 Authenticated {
123 user_id: Uuid::now_v7(),
124 method: AuthMethod::ApiKey {
125 key_id: Uuid::now_v7(),
126 key_name: "key".to_string(),
127 scopes,
128 owner_is_admin,
129 },
130 }
131 }
132
133 #[test]
134 fn scope_jwt_member_on_admin_only_route_is_forbidden() {
135 let result = jwt(false).require(ApiKeyScope::RunsManage, true);
136 assert!(matches!(result, Err(ApiError::Forbidden)));
137 }
138
139 #[test]
140 fn scope_jwt_member_on_regular_route_passes() {
141 assert!(jwt(false).require(ApiKeyScope::RunsRead, false).is_ok());
142 }
143
144 #[test]
145 fn scope_jwt_admin_passes_admin_only_route() {
146 assert!(jwt(true).require(ApiKeyScope::Admin, true).is_ok());
147 }
148
149 #[test]
150 fn scope_key_with_another_scope_is_insufficient() {
151 let auth = key(vec![ApiKeyScope::StatsRead], true);
152 let result = auth.require(ApiKeyScope::RunsRead, false);
153 assert!(matches!(result, Err(ApiError::InsufficientScope)));
154 }
155
156 #[test]
157 fn scope_key_with_the_exact_scope_passes() {
158 let auth = key(vec![ApiKeyScope::RunsRead], false);
159 assert!(auth.require(ApiKeyScope::RunsRead, false).is_ok());
160 }
161
162 #[test]
163 fn scope_admin_key_satisfies_any_scope() {
164 let auth = key(vec![ApiKeyScope::Admin], true);
165 for scope in ApiKeyScope::all_non_admin() {
166 assert!(auth.require(scope, false).is_ok());
167 }
168 assert!(auth.require(ApiKeyScope::Admin, true).is_ok());
169 }
170
171 #[test]
172 fn scope_key_of_member_on_admin_only_route_is_forbidden() {
173 let auth = key(vec![ApiKeyScope::RunsManage], false);
174 let result = auth.require(ApiKeyScope::RunsManage, true);
175 assert!(matches!(result, Err(ApiError::Forbidden)));
176 }
177
178 #[test]
179 fn scope_is_checked_before_the_owner_admin_flag() {
180 let auth = key(vec![ApiKeyScope::StatsRead], false);
181 let result = auth.require(ApiKeyScope::RunsManage, true);
182 assert!(matches!(result, Err(ApiError::InsufficientScope)));
183 }
184
185 #[test]
186 fn scope_admin_key_of_member_is_forbidden_on_admin_only_route() {
187 let auth = key(vec![ApiKeyScope::Admin], false);
188 let result = auth.require(ApiKeyScope::Admin, true);
189 assert!(matches!(result, Err(ApiError::Forbidden)));
190 }
191}