use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ApiKeyScope {
WorkflowsRead,
RunsRead,
RunsWrite,
RunsManage,
StatsRead,
Admin,
}
impl ApiKeyScope {
pub fn permits(&self, required: &ApiKeyScope) -> bool {
match self {
ApiKeyScope::Admin => true,
other => other == required,
}
}
pub fn has_permission(scopes: &[ApiKeyScope], required: &ApiKeyScope) -> bool {
scopes.iter().any(|s| s.permits(required))
}
pub fn all_non_admin() -> Vec<ApiKeyScope> {
vec![
ApiKeyScope::WorkflowsRead,
ApiKeyScope::RunsRead,
ApiKeyScope::RunsWrite,
ApiKeyScope::RunsManage,
ApiKeyScope::StatsRead,
]
}
pub fn member_allowed() -> &'static [ApiKeyScope] {
&[
ApiKeyScope::WorkflowsRead,
ApiKeyScope::RunsRead,
ApiKeyScope::StatsRead,
]
}
pub fn all_allowed_for_member(scopes: &[ApiKeyScope]) -> bool {
let allowed = Self::member_allowed();
scopes.iter().all(|s| allowed.contains(s))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn admin_permits_everything() {
let admin = ApiKeyScope::Admin;
assert!(admin.permits(&ApiKeyScope::RunsRead));
assert!(admin.permits(&ApiKeyScope::RunsWrite));
assert!(admin.permits(&ApiKeyScope::RunsManage));
assert!(admin.permits(&ApiKeyScope::WorkflowsRead));
assert!(admin.permits(&ApiKeyScope::StatsRead));
assert!(admin.permits(&ApiKeyScope::Admin));
}
#[test]
fn regular_scope_only_permits_itself() {
let scope = ApiKeyScope::RunsRead;
assert!(scope.permits(&ApiKeyScope::RunsRead));
assert!(!scope.permits(&ApiKeyScope::RunsWrite));
assert!(!scope.permits(&ApiKeyScope::Admin));
}
#[test]
fn has_permission_with_multiple_scopes() {
let scopes = vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead];
assert!(ApiKeyScope::has_permission(&scopes, &ApiKeyScope::RunsRead));
assert!(ApiKeyScope::has_permission(
&scopes,
&ApiKeyScope::WorkflowsRead
));
assert!(!ApiKeyScope::has_permission(
&scopes,
&ApiKeyScope::RunsWrite
));
}
#[test]
fn roundtrip_display_parse() {
let scopes = vec![
ApiKeyScope::WorkflowsRead,
ApiKeyScope::RunsRead,
ApiKeyScope::RunsWrite,
ApiKeyScope::RunsManage,
ApiKeyScope::StatsRead,
ApiKeyScope::Admin,
];
for scope in scopes {
let s = scope.to_string();
let parsed: ApiKeyScope = s.parse().expect("should parse");
assert_eq!(parsed, scope);
}
}
#[test]
fn parse_invalid_scope() {
let result = "invalid".parse::<ApiKeyScope>();
assert!(result.is_err());
}
#[test]
fn serde_roundtrip() {
let scope = ApiKeyScope::RunsWrite;
let json = serde_json::to_string(&scope).expect("serialize");
assert_eq!(json, "\"runs_write\"");
let parsed: ApiKeyScope = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, scope);
}
#[test]
fn all_non_admin_excludes_admin() {
let scopes = ApiKeyScope::all_non_admin();
assert!(!scopes.contains(&ApiKeyScope::Admin));
assert_eq!(scopes.len(), 5);
}
#[test]
fn member_allowed_is_read_only() {
let allowed = ApiKeyScope::member_allowed();
assert!(allowed.contains(&ApiKeyScope::WorkflowsRead));
assert!(allowed.contains(&ApiKeyScope::RunsRead));
assert!(allowed.contains(&ApiKeyScope::StatsRead));
assert!(!allowed.contains(&ApiKeyScope::RunsWrite));
assert!(!allowed.contains(&ApiKeyScope::RunsManage));
assert!(!allowed.contains(&ApiKeyScope::Admin));
}
#[test]
fn all_allowed_for_member_accepts_read_scopes() {
let scopes = vec![ApiKeyScope::WorkflowsRead, ApiKeyScope::RunsRead];
assert!(ApiKeyScope::all_allowed_for_member(&scopes));
}
#[test]
fn all_allowed_for_member_rejects_write_scopes() {
let scopes = vec![ApiKeyScope::RunsRead, ApiKeyScope::RunsWrite];
assert!(!ApiKeyScope::all_allowed_for_member(&scopes));
}
#[test]
fn all_allowed_for_member_rejects_admin() {
let scopes = vec![ApiKeyScope::Admin];
assert!(!ApiKeyScope::all_allowed_for_member(&scopes));
}
}