Skip to main content

hackamore_models/
lib.rs

1//! Generated protocol/contract types for hackamore (see `fluorite/*.fl`).
2//!
3//! Each module is generated from the like-named schema package. Hand-written
4//! convenience constructors live here, never in the schemas.
5
6#[allow(clippy::doc_markdown, clippy::too_many_arguments)]
7pub mod action {
8    include!(concat!(env!("OUT_DIR"), "/action/mod.rs"));
9}
10
11#[allow(clippy::doc_markdown, clippy::too_many_arguments)]
12pub mod policy {
13    include!(concat!(env!("OUT_DIR"), "/policy/mod.rs"));
14}
15
16#[allow(clippy::doc_markdown, clippy::too_many_arguments)]
17pub mod verdict {
18    include!(concat!(env!("OUT_DIR"), "/verdict/mod.rs"));
19}
20
21#[allow(clippy::doc_markdown, clippy::too_many_arguments)]
22pub mod audit {
23    include!(concat!(env!("OUT_DIR"), "/audit/mod.rs"));
24}
25
26#[allow(clippy::doc_markdown, clippy::too_many_arguments)]
27pub mod control {
28    include!(concat!(env!("OUT_DIR"), "/control/mod.rs"));
29}
30
31#[allow(clippy::doc_markdown, clippy::too_many_arguments)]
32pub mod provision {
33    include!(concat!(env!("OUT_DIR"), "/provision/mod.rs"));
34}
35
36/// An empty `fields` JSON object — the default when a request carries no query or body
37/// attributes relevant to conditional rules.
38pub fn empty_fields() -> serde_json::Value {
39    serde_json::Value::Object(serde_json::Map::new())
40}
41
42impl action::Action {
43    /// Ergonomic constructor with an empty `fields` object (the generated `new` requires
44    /// every field, including `fields`, positionally). `target` is the service instance
45    /// name.
46    pub fn of(target: impl Into<String>, verb: action::Verb, resource: action::Resource) -> Self {
47        Self {
48            target: target.into(),
49            verb,
50            resource,
51            fields: empty_fields(),
52        }
53    }
54
55    /// Set the request `fields` (merged query + body) used by conditional rules.
56    #[must_use]
57    pub fn with_fields(mut self, fields: serde_json::Value) -> Self {
58        self.fields = fields;
59        self
60    }
61}
62
63impl action::Verb {
64    /// A coarse CRUD verb (RESTful method mapping).
65    pub fn crud(kind: action::CrudKind) -> Self {
66        action::Verb::Crud(action::CrudVerb { kind })
67    }
68
69    /// A named, service-defined action (e.g. "s3:PutObject").
70    pub fn action(id: impl Into<String>) -> Self {
71        action::Verb::Action(action::NamedVerb { id: id.into() })
72    }
73
74    /// Parse a compact verb shorthand: the case-insensitive CRUD words `read`/`create`/
75    /// `update`/`delete` map to the closed [`action::CrudVerb`] arm; anything else is a
76    /// named action verb. This is the terse spelling a policy-authoring layer expands into
77    /// the verbose tagged-union JSON the wire format requires
78    /// (`{"type":"Crud","value":{"kind":"Read"}}`), so operators and call sites can write
79    /// `"read"` or `"ec2:DescribeInstances"` instead.
80    pub fn parse(s: &str) -> Self {
81        match s.to_ascii_lowercase().as_str() {
82            "read" => Self::crud(action::CrudKind::Read),
83            "create" => Self::crud(action::CrudKind::Create),
84            "update" => Self::crud(action::CrudKind::Update),
85            "delete" => Self::crud(action::CrudKind::Delete),
86            _ => Self::action(s),
87        }
88    }
89}
90
91impl action::Resource {
92    /// Ergonomic constructor accepting anything `Into<String>` (the generated `new`
93    /// takes `String` positionally).
94    pub fn of(path: impl Into<String>, kind: impl Into<String>) -> Self {
95        Self {
96            path: path.into(),
97            kind: kind.into(),
98        }
99    }
100}
101
102impl verdict::Verdict {
103    /// Whether this verdict permits the action.
104    pub fn is_allow(&self) -> bool {
105        matches!(self, verdict::Verdict::Allow(_))
106    }
107
108    /// Allow with explicit obligations. The data plane builds these from the matched
109    /// service instance (inject its credential, or pass the consumer's through).
110    pub fn allow(obligations: Vec<verdict::Obligation>) -> Self {
111        verdict::Verdict::Allow(verdict::AllowVerdict { obligations })
112    }
113
114    /// An obligation to inject the named credential upstream.
115    pub fn inject(id: impl Into<String>) -> verdict::Obligation {
116        verdict::Obligation::InjectCredential(verdict::InjectCredentialObligation {
117            credential: verdict::CredentialRef { id: id.into() },
118        })
119    }
120
121    /// An obligation to forward the consumer's own credential unchanged.
122    pub fn passthrough() -> verdict::Obligation {
123        verdict::Obligation::Passthrough(verdict::PassthroughObligation {})
124    }
125
126    /// Deny with the given reason.
127    pub fn deny(reason: verdict::DenyReason) -> Self {
128        verdict::Verdict::Deny(verdict::DenyVerdict { reason })
129    }
130}
131
132#[cfg(test)]
133#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
134mod tests {
135    use super::action::{Action, CrudKind, Resource, Verb};
136    use super::verdict::{DenyReason, Verdict};
137
138    #[test]
139    fn action_round_trips_through_json() {
140        let action = Action::of(
141            "github",
142            Verb::crud(CrudKind::Create),
143            Resource::of("repos/octocat/hello/pulls", "pull_request"),
144        )
145        .with_fields(serde_json::json!({ "base": "main" }));
146        let json = serde_json::to_string(&action).unwrap();
147        let back: Action = serde_json::from_str(&json).unwrap();
148        assert_eq!(action, back);
149    }
150
151    #[test]
152    fn verb_parse_shorthand_maps_crud_words_and_named_actions() {
153        assert_eq!(Verb::parse("read"), Verb::crud(CrudKind::Read));
154        assert_eq!(Verb::parse("DELETE"), Verb::crud(CrudKind::Delete));
155        assert_eq!(
156            Verb::parse("ec2:DescribeInstances"),
157            Verb::action("ec2:DescribeInstances")
158        );
159    }
160
161    #[test]
162    fn verb_union_supports_crud_and_named_action() {
163        let read = Verb::crud(CrudKind::Read);
164        let terminate = Verb::action("ec2:TerminateInstances");
165        assert_ne!(read, terminate);
166        let json = serde_json::to_string(&terminate).unwrap();
167        let back: Verb = serde_json::from_str(&json).unwrap();
168        assert_eq!(terminate, back);
169    }
170
171    #[test]
172    fn verdict_helpers_build_expected_variants() {
173        let allow = Verdict::allow(vec![Verdict::inject("gh")]);
174        assert!(allow.is_allow());
175
176        let passthrough = Verdict::allow(vec![Verdict::passthrough()]);
177        assert!(passthrough.is_allow());
178
179        let deny = Verdict::deny(DenyReason::NotAllowed);
180        assert!(!deny.is_allow());
181    }
182}