1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::time::Duration;
9
10use crate::crypto::{hash, Hash, PublicKey, SecretKey, Sig};
11use crate::error::{Error, Result};
12use crate::event::{ResourceId, ResourceKind};
13
14use super::principal::PrincipalId;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct CapabilityId(pub [u8; 16]);
19
20impl CapabilityId {
21 pub fn generate() -> Self {
23 use rand::RngCore;
24 let mut bytes = [0u8; 16];
25 rand::thread_rng().fill_bytes(&mut bytes);
26 Self(bytes)
27 }
28
29 pub fn from_bytes(bytes: [u8; 16]) -> Self {
31 Self(bytes)
32 }
33
34 pub fn as_bytes(&self) -> &[u8; 16] {
36 &self.0
37 }
38
39 pub fn to_hex(&self) -> String {
41 hex::encode(self.0)
42 }
43
44 pub fn from_hex(s: &str) -> Result<Self> {
46 let bytes = hex::decode(s).map_err(|_| Error::invalid_input("invalid hex"))?;
47 if bytes.len() != 16 {
48 return Err(Error::invalid_input("capability ID must be 16 bytes"));
49 }
50 let mut arr = [0u8; 16];
51 arr.copy_from_slice(&bytes);
52 Ok(Self(arr))
53 }
54}
55
56impl std::fmt::Display for CapabilityId {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{}", self.to_hex())
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum CapabilityKind {
66 Read,
69 Write,
71 Delete,
73
74 Execute,
77
78 InvokeTool { tool_id: String },
81
82 SpawnAgent,
85 DelegateCapability,
87
88 SendMessage { channel: String },
91 ReceiveMessage { channel: String },
93
94 Spend { currency: String, max_amount: u64 },
97
98 ModifyPermissions,
101 ViewAuditLog,
103}
104
105impl CapabilityKind {
106 pub fn matches(&self, other: &CapabilityKind) -> bool {
108 match (self, other) {
109 (CapabilityKind::Read, CapabilityKind::Read) => true,
110 (CapabilityKind::Write, CapabilityKind::Write) => true,
111 (CapabilityKind::Delete, CapabilityKind::Delete) => true,
112 (CapabilityKind::Execute, CapabilityKind::Execute) => true,
113 (
114 CapabilityKind::InvokeTool { tool_id: cap_tool },
115 CapabilityKind::InvokeTool {
116 tool_id: action_tool,
117 },
118 ) => cap_tool == action_tool || cap_tool == "*",
119 (CapabilityKind::SpawnAgent, CapabilityKind::SpawnAgent) => true,
120 (CapabilityKind::DelegateCapability, CapabilityKind::DelegateCapability) => true,
121 (
122 CapabilityKind::SendMessage { channel: cap_ch },
123 CapabilityKind::SendMessage { channel: action_ch },
124 ) => cap_ch == action_ch || cap_ch == "*",
125 (
126 CapabilityKind::ReceiveMessage { channel: cap_ch },
127 CapabilityKind::ReceiveMessage { channel: action_ch },
128 ) => cap_ch == action_ch || cap_ch == "*",
129 (
130 CapabilityKind::Spend {
131 currency: cap_cur,
132 max_amount: cap_max,
133 },
134 CapabilityKind::Spend {
135 currency: action_cur,
136 max_amount: action_amount,
137 },
138 ) => cap_cur == action_cur && action_amount <= cap_max,
139 (CapabilityKind::ModifyPermissions, CapabilityKind::ModifyPermissions) => true,
140 (CapabilityKind::ViewAuditLog, CapabilityKind::ViewAuditLog) => true,
141 _ => false,
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(tag = "type", rename_all = "snake_case")]
149pub enum ResourceScope {
150 Specific { resource: String },
152
153 Pattern { pattern: String },
155
156 Kind { kind: ResourceKind },
158
159 All,
161}
162
163impl ResourceScope {
164 pub fn specific(resource: impl Into<String>) -> Self {
166 Self::Specific {
167 resource: resource.into(),
168 }
169 }
170
171 pub fn pattern(pattern: impl Into<String>) -> Self {
173 Self::Pattern {
174 pattern: pattern.into(),
175 }
176 }
177
178 pub fn kind(kind: ResourceKind) -> Self {
180 Self::Kind { kind }
181 }
182
183 pub fn all() -> Self {
185 Self::All
186 }
187
188 pub fn matches(&self, resource: &ResourceId) -> bool {
190 match self {
191 ResourceScope::Specific { resource: r } => {
192 let resource_str = Self::resource_to_string(resource);
194 &resource_str == r
195 }
196 ResourceScope::Pattern { pattern } => {
197 let resource_str = Self::resource_to_string(resource);
198 self.glob_match(pattern, &resource_str)
199 }
200 ResourceScope::Kind { kind } => resource.kind == *kind,
201 ResourceScope::All => true,
202 }
203 }
204
205 fn resource_to_string(resource: &ResourceId) -> String {
207 let kind_str = match resource.kind {
208 ResourceKind::Repository => "repository",
209 ResourceKind::Commit => "commit",
210 ResourceKind::Branch => "branch",
211 ResourceKind::Tag => "tag",
212 ResourceKind::PullRequest => "pull_request",
213 ResourceKind::Issue => "issue",
214 ResourceKind::File => "file",
215 ResourceKind::User => "user",
216 ResourceKind::Organization => "organization",
217 ResourceKind::Credential => "credential",
218 ResourceKind::Config => "config",
219 ResourceKind::Document => "document",
220 ResourceKind::Other => "other",
221 };
222 format!("{}:{}", kind_str, resource.id)
223 }
224
225 fn glob_match(&self, pattern: &str, s: &str) -> bool {
227 if pattern == "*" {
228 return true;
229 }
230
231 if let Some(prefix) = pattern.strip_suffix('*') {
233 s.starts_with(prefix)
234 } else if let Some(suffix) = pattern.strip_prefix('*') {
235 s.ends_with(suffix)
236 } else {
237 pattern == s
238 }
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244pub struct RateLimit {
245 pub max_requests: u64,
247 pub period_ms: u64,
249}
250
251impl RateLimit {
252 pub fn new(max_requests: u64, period: Duration) -> Self {
254 Self {
255 max_requests,
256 period_ms: period.as_millis() as u64,
257 }
258 }
259
260 pub fn period(&self) -> Duration {
262 Duration::from_millis(self.period_ms)
263 }
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
268pub struct TimeOfDay {
269 pub hour: u8,
270 pub minute: u8,
271 pub second: u8,
272}
273
274impl TimeOfDay {
275 pub fn new(hour: u8, minute: u8, second: u8) -> Self {
277 Self {
278 hour,
279 minute,
280 second,
281 }
282 }
283
284 pub fn seconds_since_midnight(&self) -> u32 {
286 (self.hour as u32) * 3600 + (self.minute as u32) * 60 + (self.second as u32)
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(rename_all = "lowercase")]
293pub enum DayOfWeek {
294 Monday,
295 Tuesday,
296 Wednesday,
297 Thursday,
298 Friday,
299 Saturday,
300 Sunday,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct TimeWindow {
306 pub start: TimeOfDay,
308 pub end: TimeOfDay,
310 pub days: Vec<DayOfWeek>,
312 pub timezone: String,
314}
315
316impl TimeWindow {
317 pub fn weekday_business_hours() -> Self {
319 Self {
320 start: TimeOfDay::new(9, 0, 0),
321 end: TimeOfDay::new(17, 0, 0),
322 days: vec![
323 DayOfWeek::Monday,
324 DayOfWeek::Tuesday,
325 DayOfWeek::Wednesday,
326 DayOfWeek::Thursday,
327 DayOfWeek::Friday,
328 ],
329 timezone: "UTC".to_string(),
330 }
331 }
332
333 pub fn new(
335 start: TimeOfDay,
336 end: TimeOfDay,
337 days: Vec<DayOfWeek>,
338 timezone: impl Into<String>,
339 ) -> Self {
340 Self {
341 start,
342 end,
343 days,
344 timezone: timezone.into(),
345 }
346 }
347
348 pub fn is_within(&self, timestamp_ms: i64) -> bool {
357 use chrono::{Datelike, TimeZone, Timelike};
358 use chrono_tz::Tz;
359
360 let tz: Tz = match self.timezone.parse() {
362 Ok(tz) => tz,
363 Err(_) => {
364 return false;
366 }
367 };
368
369 let timestamp_secs = timestamp_ms / 1000;
371 let datetime = match tz.timestamp_opt(timestamp_secs, 0).single() {
372 Some(dt) => dt,
373 None => return false, };
375
376 let weekday = datetime.weekday();
378 let day_of_week = match weekday {
379 chrono::Weekday::Mon => DayOfWeek::Monday,
380 chrono::Weekday::Tue => DayOfWeek::Tuesday,
381 chrono::Weekday::Wed => DayOfWeek::Wednesday,
382 chrono::Weekday::Thu => DayOfWeek::Thursday,
383 chrono::Weekday::Fri => DayOfWeek::Friday,
384 chrono::Weekday::Sat => DayOfWeek::Saturday,
385 chrono::Weekday::Sun => DayOfWeek::Sunday,
386 };
387
388 if !self.days.contains(&day_of_week) {
389 return false;
390 }
391
392 let current_seconds = datetime.hour() * 3600 + datetime.minute() * 60 + datetime.second();
394
395 let start_seconds = self.start.seconds_since_midnight();
396 let end_seconds = self.end.seconds_since_midnight();
397
398 if start_seconds <= end_seconds {
400 current_seconds >= start_seconds && current_seconds < end_seconds
402 } else {
403 current_seconds >= start_seconds || current_seconds < end_seconds
405 }
406 }
407}
408
409#[derive(Debug, Clone, Default, Serialize, Deserialize)]
411pub struct CapabilityConstraints {
412 pub max_uses: Option<u64>,
414
415 pub current_uses: u64,
417
418 pub rate_limit: Option<RateLimit>,
420
421 #[serde(default)]
424 pub recent_use_timestamps: Vec<i64>,
425
426 pub time_windows: Vec<TimeWindow>,
428
429 pub requires_approval: bool,
431
432 pub approval_timeout_ms: Option<u64>,
434
435 pub custom: HashMap<String, String>,
437}
438
439impl CapabilityConstraints {
440 pub fn new() -> Self {
442 Self::default()
443 }
444
445 pub fn with_max_uses(mut self, max: u64) -> Self {
447 self.max_uses = Some(max);
448 self
449 }
450
451 pub fn with_rate_limit(mut self, limit: RateLimit) -> Self {
453 self.rate_limit = Some(limit);
454 self
455 }
456
457 pub fn with_time_window(mut self, window: TimeWindow) -> Self {
459 self.time_windows.push(window);
460 self
461 }
462
463 pub fn with_requires_approval(mut self, timeout: Duration) -> Self {
465 self.requires_approval = true;
466 self.approval_timeout_ms = Some(timeout.as_millis() as u64);
467 self
468 }
469
470 pub fn with_custom(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
472 self.custom.insert(key.into(), value.into());
473 self
474 }
475
476 pub fn approval_timeout(&self) -> Option<Duration> {
478 self.approval_timeout_ms.map(Duration::from_millis)
479 }
480
481 pub fn is_usage_limit_reached(&self) -> bool {
483 match self.max_uses {
484 Some(max) => self.current_uses >= max,
485 None => false,
486 }
487 }
488
489 pub fn increment_usage(&mut self) -> Result<()> {
491 if self.is_usage_limit_reached() {
492 return Err(Error::invalid_input("Usage limit reached"));
493 }
494 self.current_uses += 1;
495 Ok(())
496 }
497
498 pub fn is_rate_limited(&self, now: i64) -> bool {
503 match &self.rate_limit {
504 None => false,
505 Some(limit) => {
506 let window_start = now.saturating_sub(limit.period_ms as i64);
507 let recent_count = self
508 .recent_use_timestamps
509 .iter()
510 .filter(|&&ts| ts >= window_start)
511 .count();
512 recent_count >= limit.max_requests as usize
513 }
514 }
515 }
516
517 pub fn record_rate_limit_use(&mut self, now: i64) {
521 if let Some(limit) = &self.rate_limit {
522 let window_start = now.saturating_sub(limit.period_ms as i64);
523 self.recent_use_timestamps.retain(|&ts| ts >= window_start);
524 }
525 self.recent_use_timestamps.push(now);
526 }
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq)]
531pub enum CapabilityState {
532 Active,
534 Expired,
536 Revoked,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct Capability {
543 id: CapabilityId,
545
546 kind: CapabilityKind,
548
549 scope: ResourceScope,
551
552 constraints: CapabilityConstraints,
554
555 grantor: PrincipalId,
557
558 granted_at: i64,
560
561 expires_at: Option<i64>,
563
564 delegatable: bool,
566
567 max_delegation_depth: u32,
569
570 #[serde(default)]
572 delegation_depth: u32,
573
574 #[serde(default)]
576 parent_capability_id: Option<CapabilityId>,
577
578 #[serde(default)]
580 revoked_at: Option<i64>,
581
582 #[serde(default)]
584 revocation_reason: Option<String>,
585
586 signature: Sig,
588}
589
590impl Capability {
591 pub fn builder() -> CapabilityBuilder {
593 CapabilityBuilder::new()
594 }
595
596 pub fn id(&self) -> CapabilityId {
598 self.id
599 }
600
601 pub fn kind(&self) -> &CapabilityKind {
603 &self.kind
604 }
605
606 pub fn scope(&self) -> &ResourceScope {
608 &self.scope
609 }
610
611 pub fn constraints(&self) -> &CapabilityConstraints {
613 &self.constraints
614 }
615
616 pub fn constraints_mut(&mut self) -> &mut CapabilityConstraints {
618 &mut self.constraints
619 }
620
621 pub fn grantor(&self) -> &PrincipalId {
623 &self.grantor
624 }
625
626 pub fn granted_at(&self) -> i64 {
628 self.granted_at
629 }
630
631 pub fn expires_at(&self) -> Option<i64> {
633 self.expires_at
634 }
635
636 pub fn is_delegatable(&self) -> bool {
638 self.delegatable
639 }
640
641 pub fn max_delegation_depth(&self) -> u32 {
643 self.max_delegation_depth
644 }
645
646 pub fn signature(&self) -> &Sig {
648 &self.signature
649 }
650
651 pub fn delegation_depth(&self) -> u32 {
653 self.delegation_depth
654 }
655
656 pub fn parent_capability_id(&self) -> Option<CapabilityId> {
658 self.parent_capability_id
659 }
660
661 pub fn revoke(&mut self, reason: impl Into<String>) {
663 self.revoked_at = Some(chrono::Utc::now().timestamp_millis());
664 self.revocation_reason = Some(reason.into());
665 }
666
667 pub fn is_revoked(&self) -> bool {
669 self.revoked_at.is_some()
670 }
671
672 pub fn revoked_at(&self) -> Option<i64> {
674 self.revoked_at
675 }
676
677 pub fn revocation_reason(&self) -> Option<&str> {
679 self.revocation_reason.as_deref()
680 }
681
682 pub fn lifecycle_state(&self, now_ms: i64) -> CapabilityState {
684 if self.is_revoked() {
685 CapabilityState::Revoked
686 } else if let Some(exp) = self.expires_at {
687 if now_ms >= exp {
688 CapabilityState::Expired
689 } else {
690 CapabilityState::Active
691 }
692 } else {
693 CapabilityState::Active
694 }
695 }
696
697 pub fn is_valid_at(&self, timestamp: i64) -> bool {
701 if self.is_revoked() {
702 return false;
703 }
704 match self.expires_at {
705 Some(exp) => timestamp < exp,
706 None => true, }
708 }
709
710 pub fn matches(&self, action_kind: &CapabilityKind, resource: &ResourceId) -> bool {
712 self.kind.matches(action_kind) && self.scope.matches(resource)
713 }
714
715 pub fn canonical_bytes(&self) -> Vec<u8> {
717 let mut data = Vec::new();
718 data.extend_from_slice(&self.id.0);
719 let kind_json = serde_json::to_vec(&self.kind).unwrap_or_default();
721 data.extend_from_slice(&kind_json);
722 let scope_json = serde_json::to_vec(&self.scope).unwrap_or_default();
723 data.extend_from_slice(&scope_json);
724 let grantor_json = serde_json::to_vec(&self.grantor).unwrap_or_default();
725 data.extend_from_slice(&grantor_json);
726 data.extend_from_slice(&self.granted_at.to_le_bytes());
727 if let Some(exp) = self.expires_at {
728 data.extend_from_slice(&exp.to_le_bytes());
729 }
730 data.push(if self.delegatable { 1 } else { 0 });
731 data.extend_from_slice(&self.max_delegation_depth.to_le_bytes());
732 data.extend_from_slice(&self.delegation_depth.to_le_bytes());
733 if let Some(parent_id) = &self.parent_capability_id {
734 data.extend_from_slice(&parent_id.0);
735 }
736 data
737 }
738
739 pub fn hash(&self) -> Hash {
741 hash(&self.canonical_bytes())
742 }
743
744 pub fn delegate(
751 &self,
752 delegator_key: &SecretKey,
753 scope: Option<ResourceScope>,
754 expiry: Option<Duration>,
755 ) -> Result<Capability> {
756 if !self.delegatable {
757 return Err(Error::invalid_input("capability is not delegatable"));
758 }
759
760 if self.delegation_depth + 1 > self.max_delegation_depth {
761 return Err(Error::invalid_input(format!(
762 "delegation depth {} would exceed max {}",
763 self.delegation_depth + 1,
764 self.max_delegation_depth
765 )));
766 }
767
768 let child_scope = match scope {
770 Some(s) => {
771 if !Self::is_scope_subset(&s, &self.scope) {
772 return Err(Error::invalid_input(
773 "child scope must be a subset of parent scope",
774 ));
775 }
776 s
777 }
778 None => self.scope.clone(),
779 };
780
781 let child_expires_at = match expiry {
783 Some(dur) => {
784 let now = chrono::Utc::now().timestamp_millis();
785 let proposed = now + dur.as_millis() as i64;
786 if let Some(parent_exp) = self.expires_at {
787 if proposed > parent_exp {
788 return Err(Error::invalid_input(
789 "child expiry must not exceed parent expiry",
790 ));
791 }
792 }
793 Some(proposed)
794 }
795 None => self.expires_at,
796 };
797
798 let mut child = Capability {
799 id: CapabilityId::generate(),
800 kind: self.kind.clone(),
801 scope: child_scope,
802 constraints: CapabilityConstraints::default(),
803 grantor: self.grantor.clone(),
804 granted_at: chrono::Utc::now().timestamp_millis(),
805 expires_at: child_expires_at,
806 delegatable: self.delegatable,
807 max_delegation_depth: self.max_delegation_depth,
808 delegation_depth: self.delegation_depth + 1,
809 parent_capability_id: Some(self.id),
810 revoked_at: None,
811 revocation_reason: None,
812 signature: Sig::empty(),
813 };
814
815 let canonical = child.canonical_bytes();
816 child.signature = delegator_key.sign(&canonical);
817
818 Ok(child)
819 }
820
821 fn is_scope_subset(child: &ResourceScope, parent: &ResourceScope) -> bool {
823 match (child, parent) {
824 (ResourceScope::All, ResourceScope::All) => true,
826 (ResourceScope::All, _) => false,
827 (_, ResourceScope::All) => true,
829 (ResourceScope::Specific { resource: c }, ResourceScope::Specific { resource: p }) => {
831 c == p
832 }
833 (ResourceScope::Specific { resource: c }, ResourceScope::Pattern { pattern: p }) => {
835 p.ends_with('*') && c.starts_with(&p[..p.len() - 1]) || c == p
836 }
837 (ResourceScope::Pattern { pattern: c }, ResourceScope::Pattern { pattern: p }) => {
839 p.ends_with('*') && c.starts_with(&p[..p.len() - 1]) || c == p
840 }
841 (ResourceScope::Kind { kind: c }, ResourceScope::Kind { kind: p }) => c == p,
843 _ => false,
845 }
846 }
847}
848
849#[derive(Debug, Default)]
851pub struct CapabilityBuilder {
852 id: Option<CapabilityId>,
853 kind: Option<CapabilityKind>,
854 scope: Option<ResourceScope>,
855 constraints: CapabilityConstraints,
856 grantor: Option<PrincipalId>,
857 granted_at: Option<i64>,
858 expires_at: Option<i64>,
859 delegatable: bool,
860 max_delegation_depth: u32,
861}
862
863impl CapabilityBuilder {
864 pub fn new() -> Self {
866 Self {
867 max_delegation_depth: 3, ..Default::default()
869 }
870 }
871
872 pub fn id(mut self, id: CapabilityId) -> Self {
874 self.id = Some(id);
875 self
876 }
877
878 pub fn kind(mut self, kind: CapabilityKind) -> Self {
880 self.kind = Some(kind);
881 self
882 }
883
884 pub fn scope(mut self, scope: ResourceScope) -> Self {
886 self.scope = Some(scope);
887 self
888 }
889
890 pub fn constraints(mut self, constraints: CapabilityConstraints) -> Self {
892 self.constraints = constraints;
893 self
894 }
895
896 pub fn grantor(mut self, grantor: PrincipalId) -> Self {
898 self.grantor = Some(grantor);
899 self
900 }
901
902 pub fn granted_at(mut self, timestamp: i64) -> Self {
904 self.granted_at = Some(timestamp);
905 self
906 }
907
908 pub fn expires_at(mut self, timestamp: i64) -> Self {
910 self.expires_at = Some(timestamp);
911 self
912 }
913
914 pub fn expires_in(mut self, duration: Duration) -> Self {
916 let now = chrono::Utc::now().timestamp_millis();
917 self.expires_at = Some(now + duration.as_millis() as i64);
918 self
919 }
920
921 pub fn delegatable(mut self, max_depth: u32) -> Self {
923 self.delegatable = true;
924 self.max_delegation_depth = max_depth;
925 self
926 }
927
928 pub fn sign(self, _grantor_key: &SecretKey) -> Result<Capability> {
930 let id = self.id.unwrap_or_else(CapabilityId::generate);
931
932 let kind = self
933 .kind
934 .ok_or_else(|| Error::invalid_input("kind is required"))?;
935
936 let scope = self.scope.unwrap_or(ResourceScope::All);
937
938 let grantor = self
939 .grantor
940 .ok_or_else(|| Error::invalid_input("grantor is required"))?;
941
942 let granted_at = self
943 .granted_at
944 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
945
946 let mut capability = Capability {
947 id,
948 kind,
949 scope,
950 constraints: self.constraints,
951 grantor,
952 granted_at,
953 expires_at: self.expires_at,
954 delegatable: self.delegatable,
955 max_delegation_depth: self.max_delegation_depth,
956 delegation_depth: 0,
957 parent_capability_id: None,
958 revoked_at: None,
959 revocation_reason: None,
960 signature: Sig::empty(),
961 };
962
963 let canonical = capability.canonical_bytes();
965 capability.signature = _grantor_key.sign(&canonical);
966
967 Ok(capability)
968 }
969}
970
971#[derive(Debug, Clone, PartialEq, Eq)]
973pub enum CapabilityCheck {
974 Permitted { capability_id: CapabilityId },
976 Denied { reason: DenialReason },
978 RequiresApproval {
980 capability_id: CapabilityId,
981 timeout: Duration,
982 },
983}
984
985#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(rename_all = "snake_case")]
988pub enum DenialReason {
989 NoMatchingCapability,
991 Expired,
993 RateLimitExceeded,
995 UsageLimitExceeded,
997 OutsideTimeWindow,
999 ScopeViolation,
1001 DelegationDepthExceeded,
1003 Revoked,
1005}
1006
1007impl std::fmt::Display for DenialReason {
1008 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1009 match self {
1010 DenialReason::NoMatchingCapability => write!(f, "No matching capability found"),
1011 DenialReason::Expired => write!(f, "Capability has expired"),
1012 DenialReason::RateLimitExceeded => write!(f, "Rate limit exceeded"),
1013 DenialReason::UsageLimitExceeded => write!(f, "Usage limit exceeded"),
1014 DenialReason::OutsideTimeWindow => write!(f, "Outside allowed time window"),
1015 DenialReason::ScopeViolation => write!(f, "Resource not in capability scope"),
1016 DenialReason::DelegationDepthExceeded => write!(f, "Delegation depth exceeded"),
1017 DenialReason::Revoked => write!(f, "Capability has been revoked"),
1018 }
1019 }
1020}
1021
1022#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1024pub struct CapabilitySetId(pub Hash);
1025
1026#[derive(Debug, Clone, Serialize, Deserialize)]
1028pub struct CapabilitySet {
1029 capabilities: Vec<Capability>,
1031
1032 grantee: PublicKey,
1034
1035 parent: Option<CapabilitySetId>,
1037
1038 delegation_depth: u32,
1040}
1041
1042impl CapabilitySet {
1043 pub fn new(grantee: PublicKey) -> Self {
1045 Self {
1046 capabilities: Vec::new(),
1047 grantee,
1048 parent: None,
1049 delegation_depth: 0,
1050 }
1051 }
1052
1053 pub fn with_capabilities(grantee: PublicKey, capabilities: Vec<Capability>) -> Self {
1055 Self {
1056 capabilities,
1057 grantee,
1058 parent: None,
1059 delegation_depth: 0,
1060 }
1061 }
1062
1063 pub fn add(&mut self, capability: Capability) {
1065 self.capabilities.push(capability);
1066 }
1067
1068 pub fn grantee(&self) -> &PublicKey {
1070 &self.grantee
1071 }
1072
1073 pub fn capabilities(&self) -> &[Capability] {
1075 &self.capabilities
1076 }
1077
1078 pub fn parent(&self) -> Option<CapabilitySetId> {
1080 self.parent
1081 }
1082
1083 pub fn delegation_depth(&self) -> u32 {
1085 self.delegation_depth
1086 }
1087
1088 pub fn hash(&self) -> Hash {
1090 let mut data = Vec::new();
1091 data.extend_from_slice(&self.grantee.as_bytes());
1092 for cap in &self.capabilities {
1093 data.extend_from_slice(cap.hash().as_bytes());
1094 }
1095 hash(&data)
1096 }
1097
1098 pub fn id(&self) -> CapabilitySetId {
1100 CapabilitySetId(self.hash())
1101 }
1102
1103 pub fn permits(
1105 &self,
1106 action_kind: &CapabilityKind,
1107 resource: &ResourceId,
1108 timestamp: i64,
1109 ) -> CapabilityCheck {
1110 for cap in &self.capabilities {
1111 if !cap.matches(action_kind, resource) {
1113 continue;
1114 }
1115
1116 if cap.is_revoked() {
1118 return CapabilityCheck::Denied {
1119 reason: DenialReason::Revoked,
1120 };
1121 }
1122
1123 if !cap.is_valid_at(timestamp) {
1125 return CapabilityCheck::Denied {
1126 reason: DenialReason::Expired,
1127 };
1128 }
1129
1130 if cap.constraints().is_usage_limit_reached() {
1132 return CapabilityCheck::Denied {
1133 reason: DenialReason::UsageLimitExceeded,
1134 };
1135 }
1136
1137 if cap.constraints().is_rate_limited(timestamp) {
1139 return CapabilityCheck::Denied {
1140 reason: DenialReason::RateLimitExceeded,
1141 };
1142 }
1143
1144 if !cap.constraints().time_windows.is_empty() {
1146 let in_window = cap
1147 .constraints()
1148 .time_windows
1149 .iter()
1150 .any(|w| w.is_within(timestamp));
1151 if !in_window {
1152 return CapabilityCheck::Denied {
1153 reason: DenialReason::OutsideTimeWindow,
1154 };
1155 }
1156 }
1157
1158 if cap.constraints().requires_approval {
1160 let timeout = cap
1161 .constraints()
1162 .approval_timeout()
1163 .unwrap_or(Duration::from_secs(300));
1164 return CapabilityCheck::RequiresApproval {
1165 capability_id: cap.id(),
1166 timeout,
1167 };
1168 }
1169
1170 return CapabilityCheck::Permitted {
1172 capability_id: cap.id(),
1173 };
1174 }
1175
1176 CapabilityCheck::Denied {
1178 reason: DenialReason::NoMatchingCapability,
1179 }
1180 }
1181
1182 pub fn find_capability(
1184 &self,
1185 action_kind: &CapabilityKind,
1186 resource: &ResourceId,
1187 ) -> Option<&Capability> {
1188 self.capabilities
1189 .iter()
1190 .find(|cap| cap.matches(action_kind, resource))
1191 }
1192
1193 pub fn delegate(
1195 &self,
1196 capability_ids: Vec<CapabilityId>,
1197 new_grantee: PublicKey,
1198 ) -> Result<CapabilitySet> {
1199 let mut delegated_caps = Vec::new();
1200
1201 for id in capability_ids {
1202 let cap = self
1203 .capabilities
1204 .iter()
1205 .find(|c| c.id() == id)
1206 .ok_or_else(|| Error::invalid_input("Capability not found in set"))?;
1207
1208 if !cap.is_delegatable() {
1209 return Err(Error::invalid_input("Capability is not delegatable"));
1210 }
1211
1212 if self.delegation_depth + 1 > cap.max_delegation_depth() {
1213 return Err(Error::invalid_input("Delegation depth exceeded"));
1214 }
1215
1216 delegated_caps.push(cap.clone());
1217 }
1218
1219 Ok(CapabilitySet {
1220 capabilities: delegated_caps,
1221 grantee: new_grantee,
1222 parent: Some(self.id()),
1223 delegation_depth: self.delegation_depth + 1,
1224 })
1225 }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use super::*;
1231 use crate::crypto::SecretKey;
1232
1233 fn test_grantor() -> PrincipalId {
1234 PrincipalId::user("test-user").unwrap()
1235 }
1236
1237 fn test_resource(kind: &str, id: &str) -> ResourceId {
1238 let kind = match kind {
1239 "repo" | "repository" => ResourceKind::Repository,
1240 "file" => ResourceKind::File,
1241 "commit" => ResourceKind::Commit,
1242 "branch" => ResourceKind::Branch,
1243 _ => ResourceKind::Other,
1244 };
1245 ResourceId::new(kind, id)
1246 }
1247
1248 #[test]
1251 fn capability_id_generates_unique() {
1252 let id1 = CapabilityId::generate();
1253 let id2 = CapabilityId::generate();
1254 assert_ne!(id1, id2);
1255 }
1256
1257 #[test]
1258 fn capability_id_hex_roundtrip() {
1259 let id = CapabilityId::generate();
1260 let hex = id.to_hex();
1261 let restored = CapabilityId::from_hex(&hex).unwrap();
1262 assert_eq!(id, restored);
1263 }
1264
1265 #[test]
1268 fn scope_specific_matches_exact_resource() {
1269 let scope = ResourceScope::specific("repository:org/project");
1270 assert!(scope.matches(&test_resource("repository", "org/project")));
1271 assert!(!scope.matches(&test_resource("repository", "org/other")));
1272 }
1273
1274 #[test]
1275 fn scope_pattern_matches_glob() {
1276 let scope = ResourceScope::pattern("repository:org/*");
1277 assert!(scope.matches(&test_resource("repository", "org/project")));
1278 assert!(scope.matches(&test_resource("repository", "org/other")));
1279 assert!(!scope.matches(&test_resource("repository", "other/project")));
1280 }
1281
1282 #[test]
1283 fn scope_kind_matches_all_of_kind() {
1284 let scope = ResourceScope::kind(ResourceKind::Repository);
1285 assert!(scope.matches(&test_resource("repository", "anything")));
1286 assert!(!scope.matches(&test_resource("file", "anything")));
1287 }
1288
1289 #[test]
1290 fn scope_all_matches_everything() {
1291 let scope = ResourceScope::all();
1292 assert!(scope.matches(&test_resource("repository", "anything")));
1293 assert!(scope.matches(&test_resource("file", "anything")));
1294 }
1295
1296 #[test]
1299 fn constraint_max_uses_enforced() {
1300 let mut constraints = CapabilityConstraints::new().with_max_uses(5);
1301
1302 for _ in 0..5 {
1303 assert!(constraints.increment_usage().is_ok());
1304 }
1305 assert!(constraints.increment_usage().is_err());
1306 }
1307
1308 #[test]
1309 fn constraint_unlimited_uses() {
1310 let mut constraints = CapabilityConstraints::new();
1311
1312 for _ in 0..1000 {
1313 assert!(constraints.increment_usage().is_ok());
1314 }
1315 }
1316
1317 #[test]
1320 fn capability_kind_matches_same() {
1321 assert!(CapabilityKind::Read.matches(&CapabilityKind::Read));
1322 assert!(CapabilityKind::Write.matches(&CapabilityKind::Write));
1323 assert!(CapabilityKind::Execute.matches(&CapabilityKind::Execute));
1324 }
1325
1326 #[test]
1327 fn capability_kind_different_not_match() {
1328 assert!(!CapabilityKind::Read.matches(&CapabilityKind::Write));
1329 assert!(!CapabilityKind::Write.matches(&CapabilityKind::Execute));
1330 }
1331
1332 #[test]
1333 fn capability_kind_tool_matches_specific() {
1334 let cap_kind = CapabilityKind::InvokeTool {
1335 tool_id: "bash".to_string(),
1336 };
1337 let action_kind = CapabilityKind::InvokeTool {
1338 tool_id: "bash".to_string(),
1339 };
1340 assert!(cap_kind.matches(&action_kind));
1341
1342 let other_tool = CapabilityKind::InvokeTool {
1343 tool_id: "read_file".to_string(),
1344 };
1345 assert!(!cap_kind.matches(&other_tool));
1346 }
1347
1348 #[test]
1349 fn capability_kind_tool_wildcard() {
1350 let cap_kind = CapabilityKind::InvokeTool {
1351 tool_id: "*".to_string(),
1352 };
1353 let action_kind = CapabilityKind::InvokeTool {
1354 tool_id: "bash".to_string(),
1355 };
1356 assert!(cap_kind.matches(&action_kind));
1357 }
1358
1359 #[test]
1360 fn capability_kind_spend_checks_amount() {
1361 let cap_kind = CapabilityKind::Spend {
1362 currency: "USD".to_string(),
1363 max_amount: 100,
1364 };
1365 let action_ok = CapabilityKind::Spend {
1366 currency: "USD".to_string(),
1367 max_amount: 50,
1368 };
1369 let action_too_much = CapabilityKind::Spend {
1370 currency: "USD".to_string(),
1371 max_amount: 150,
1372 };
1373
1374 assert!(cap_kind.matches(&action_ok));
1375 assert!(!cap_kind.matches(&action_too_much));
1376 }
1377
1378 #[test]
1381 fn capability_created_successfully() {
1382 let grantor_key = SecretKey::generate();
1383 let cap = Capability::builder()
1384 .kind(CapabilityKind::Read)
1385 .scope(ResourceScope::all())
1386 .grantor(test_grantor())
1387 .sign(&grantor_key)
1388 .unwrap();
1389
1390 assert_eq!(cap.kind(), &CapabilityKind::Read);
1391 assert_eq!(cap.scope(), &ResourceScope::all());
1392 }
1393
1394 #[test]
1395 fn capability_expiry_checked() {
1396 let grantor_key = SecretKey::generate();
1397 let now = chrono::Utc::now().timestamp_millis();
1398
1399 let cap = Capability::builder()
1400 .kind(CapabilityKind::Read)
1401 .scope(ResourceScope::all())
1402 .grantor(test_grantor())
1403 .granted_at(now)
1404 .expires_at(now + 3600 * 1000)
1405 .sign(&grantor_key)
1406 .unwrap();
1407
1408 assert!(cap.is_valid_at(now));
1409 assert!(cap.is_valid_at(now + 1800 * 1000));
1410 assert!(!cap.is_valid_at(now + 3600 * 1000));
1411 }
1412
1413 #[test]
1414 fn capability_matches_action() {
1415 let grantor_key = SecretKey::generate();
1416 let cap = Capability::builder()
1417 .kind(CapabilityKind::Read)
1418 .scope(ResourceScope::kind(ResourceKind::Repository))
1419 .grantor(test_grantor())
1420 .sign(&grantor_key)
1421 .unwrap();
1422
1423 assert!(cap.matches(&CapabilityKind::Read, &test_resource("repository", "test")));
1424 assert!(!cap.matches(&CapabilityKind::Write, &test_resource("repository", "test")));
1425 assert!(!cap.matches(&CapabilityKind::Read, &test_resource("file", "test")));
1426 }
1427
1428 #[test]
1431 fn capability_set_permits_matching() {
1432 let grantor_key = SecretKey::generate();
1433 let agent_key = SecretKey::generate();
1434
1435 let cap = Capability::builder()
1436 .kind(CapabilityKind::Read)
1437 .scope(ResourceScope::all())
1438 .grantor(test_grantor())
1439 .sign(&grantor_key)
1440 .unwrap();
1441
1442 let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1443 let now = chrono::Utc::now().timestamp_millis();
1444
1445 let check = set.permits(
1446 &CapabilityKind::Read,
1447 &test_resource("repository", "test"),
1448 now,
1449 );
1450
1451 assert!(matches!(check, CapabilityCheck::Permitted { .. }));
1452 }
1453
1454 #[test]
1455 fn capability_set_denies_no_matching() {
1456 let grantor_key = SecretKey::generate();
1457 let agent_key = SecretKey::generate();
1458
1459 let cap = Capability::builder()
1460 .kind(CapabilityKind::Read)
1461 .scope(ResourceScope::all())
1462 .grantor(test_grantor())
1463 .sign(&grantor_key)
1464 .unwrap();
1465
1466 let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1467 let now = chrono::Utc::now().timestamp_millis();
1468
1469 let check = set.permits(
1470 &CapabilityKind::Write,
1471 &test_resource("repository", "test"),
1472 now,
1473 );
1474
1475 assert!(matches!(
1476 check,
1477 CapabilityCheck::Denied {
1478 reason: DenialReason::NoMatchingCapability
1479 }
1480 ));
1481 }
1482
1483 #[test]
1484 fn capability_set_requires_approval() {
1485 let grantor_key = SecretKey::generate();
1486 let agent_key = SecretKey::generate();
1487
1488 let cap = Capability::builder()
1489 .kind(CapabilityKind::Write)
1490 .scope(ResourceScope::all())
1491 .grantor(test_grantor())
1492 .constraints(
1493 CapabilityConstraints::new().with_requires_approval(Duration::from_secs(300)),
1494 )
1495 .sign(&grantor_key)
1496 .unwrap();
1497
1498 let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1499 let now = chrono::Utc::now().timestamp_millis();
1500
1501 let check = set.permits(
1502 &CapabilityKind::Write,
1503 &test_resource("repository", "test"),
1504 now,
1505 );
1506
1507 assert!(matches!(check, CapabilityCheck::RequiresApproval { .. }));
1508 }
1509
1510 #[test]
1511 fn capability_set_denies_expired() {
1512 let grantor_key = SecretKey::generate();
1513 let agent_key = SecretKey::generate();
1514 let now = chrono::Utc::now().timestamp_millis();
1515
1516 let cap = Capability::builder()
1517 .kind(CapabilityKind::Read)
1518 .scope(ResourceScope::all())
1519 .grantor(test_grantor())
1520 .granted_at(now - 7200 * 1000)
1521 .expires_at(now - 3600 * 1000) .sign(&grantor_key)
1523 .unwrap();
1524
1525 let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1526
1527 let check = set.permits(
1528 &CapabilityKind::Read,
1529 &test_resource("repository", "test"),
1530 now,
1531 );
1532
1533 assert!(matches!(
1534 check,
1535 CapabilityCheck::Denied {
1536 reason: DenialReason::Expired
1537 }
1538 ));
1539 }
1540
1541 #[test]
1542 fn capability_set_delegation() {
1543 let grantor_key = SecretKey::generate();
1544 let agent1_key = SecretKey::generate();
1545 let agent2_key = SecretKey::generate();
1546
1547 let cap = Capability::builder()
1548 .kind(CapabilityKind::Read)
1549 .scope(ResourceScope::all())
1550 .grantor(test_grantor())
1551 .delegatable(3)
1552 .sign(&grantor_key)
1553 .unwrap();
1554
1555 let cap_id = cap.id();
1556 let set = CapabilitySet::with_capabilities(agent1_key.public_key(), vec![cap]);
1557
1558 let delegated = set.delegate(vec![cap_id], agent2_key.public_key()).unwrap();
1559
1560 assert_eq!(delegated.grantee(), &agent2_key.public_key());
1561 assert_eq!(delegated.delegation_depth(), 1);
1562 assert_eq!(delegated.capabilities().len(), 1);
1563 }
1564
1565 #[test]
1566 fn capability_set_delegation_depth_enforced() {
1567 let grantor_key = SecretKey::generate();
1568 let agent1_key = SecretKey::generate();
1569 let agent2_key = SecretKey::generate();
1570 let agent3_key = SecretKey::generate();
1571
1572 let cap = Capability::builder()
1574 .kind(CapabilityKind::Read)
1575 .scope(ResourceScope::all())
1576 .grantor(test_grantor())
1577 .delegatable(1)
1578 .sign(&grantor_key)
1579 .unwrap();
1580
1581 let cap_id = cap.id();
1582 let set1 = CapabilitySet::with_capabilities(agent1_key.public_key(), vec![cap]);
1583
1584 let set2 = set1
1586 .delegate(vec![cap_id], agent2_key.public_key())
1587 .unwrap();
1588
1589 let result = set2.delegate(vec![cap_id], agent3_key.public_key());
1591 assert!(result.is_err());
1592 }
1593
1594 #[test]
1595 fn capability_set_non_delegatable_rejected() {
1596 let grantor_key = SecretKey::generate();
1597 let agent1_key = SecretKey::generate();
1598 let agent2_key = SecretKey::generate();
1599
1600 let cap = Capability::builder()
1602 .kind(CapabilityKind::Read)
1603 .scope(ResourceScope::all())
1604 .grantor(test_grantor())
1605 .sign(&grantor_key)
1606 .unwrap();
1607
1608 let cap_id = cap.id();
1609 let set = CapabilitySet::with_capabilities(agent1_key.public_key(), vec![cap]);
1610
1611 let result = set.delegate(vec![cap_id], agent2_key.public_key());
1612 assert!(result.is_err());
1613 }
1614
1615 #[test]
1616 fn capability_set_hash_deterministic() {
1617 let grantor_key = SecretKey::generate();
1618 let agent_key = SecretKey::generate();
1619
1620 let cap = Capability::builder()
1621 .id(CapabilityId::from_bytes([1u8; 16]))
1622 .kind(CapabilityKind::Read)
1623 .scope(ResourceScope::all())
1624 .grantor(test_grantor())
1625 .granted_at(1000000)
1626 .sign(&grantor_key)
1627 .unwrap();
1628
1629 let set = CapabilitySet::with_capabilities(agent_key.public_key(), vec![cap]);
1630
1631 let h1 = set.hash();
1632 let h2 = set.hash();
1633 assert_eq!(h1, h2);
1634 }
1635
1636 #[test]
1639 fn time_window_within_business_hours() {
1640 let window = TimeWindow::weekday_business_hours();
1641
1642 let timestamp = 1704880800000i64;
1645 assert!(window.is_within(timestamp));
1646 }
1647
1648 #[test]
1649 fn time_window_outside_business_hours() {
1650 let window = TimeWindow::weekday_business_hours();
1651
1652 let timestamp = 1704909600000i64;
1655 assert!(!window.is_within(timestamp));
1656 }
1657
1658 #[test]
1659 fn time_window_weekend_denied() {
1660 let window = TimeWindow::weekday_business_hours();
1661
1662 let timestamp = 1705147200000i64;
1665 assert!(!window.is_within(timestamp));
1666 }
1667
1668 #[test]
1669 fn time_window_custom_timezone() {
1670 let window = TimeWindow::new(
1672 TimeOfDay::new(9, 0, 0),
1673 TimeOfDay::new(17, 0, 0),
1674 vec![DayOfWeek::Monday, DayOfWeek::Tuesday, DayOfWeek::Wednesday],
1675 "America/New_York",
1676 );
1677
1678 let timestamp_within = 1704722400000i64;
1680 assert!(window.is_within(timestamp_within));
1681
1682 let timestamp_before = 1704715200000i64;
1684 assert!(!window.is_within(timestamp_before));
1685 }
1686
1687 #[test]
1688 fn time_window_invalid_timezone_denied() {
1689 let window = TimeWindow::new(
1690 TimeOfDay::new(9, 0, 0),
1691 TimeOfDay::new(17, 0, 0),
1692 vec![DayOfWeek::Monday],
1693 "Invalid/Timezone",
1694 );
1695
1696 let timestamp = 1704722400000i64;
1698 assert!(!window.is_within(timestamp));
1699 }
1700
1701 #[test]
1702 fn time_window_overnight() {
1703 let window = TimeWindow::new(
1705 TimeOfDay::new(22, 0, 0),
1706 TimeOfDay::new(6, 0, 0),
1707 vec![
1708 DayOfWeek::Monday,
1709 DayOfWeek::Tuesday,
1710 DayOfWeek::Wednesday,
1711 DayOfWeek::Thursday,
1712 DayOfWeek::Friday,
1713 DayOfWeek::Saturday,
1714 DayOfWeek::Sunday,
1715 ],
1716 "UTC",
1717 );
1718
1719 let timestamp_late = 1704927600000i64;
1722 assert!(window.is_within(timestamp_late));
1723
1724 let timestamp_early = 1704942000000i64;
1727 assert!(window.is_within(timestamp_early));
1728
1729 let timestamp_midday = 1704888000000i64;
1731 assert!(!window.is_within(timestamp_midday));
1732 }
1733
1734 #[test]
1735 fn time_of_day_seconds_calculation() {
1736 let morning = TimeOfDay::new(9, 30, 45);
1737 assert_eq!(morning.seconds_since_midnight(), 9 * 3600 + 30 * 60 + 45);
1738
1739 let midnight = TimeOfDay::new(0, 0, 0);
1740 assert_eq!(midnight.seconds_since_midnight(), 0);
1741
1742 let end_of_day = TimeOfDay::new(23, 59, 59);
1743 assert_eq!(
1744 end_of_day.seconds_since_midnight(),
1745 23 * 3600 + 59 * 60 + 59
1746 );
1747 }
1748
1749 #[test]
1752 fn capability_revoke_transitions_to_revoked() {
1753 let key = SecretKey::generate();
1754 let mut cap = Capability::builder()
1755 .kind(CapabilityKind::Read)
1756 .scope(ResourceScope::all())
1757 .grantor(test_grantor())
1758 .sign(&key)
1759 .unwrap();
1760
1761 assert!(!cap.is_revoked());
1762 cap.revoke("policy violation");
1763 assert!(cap.is_revoked());
1764 }
1765
1766 #[test]
1767 fn capability_revoked_at_recorded() {
1768 let key = SecretKey::generate();
1769 let mut cap = Capability::builder()
1770 .kind(CapabilityKind::Read)
1771 .scope(ResourceScope::all())
1772 .grantor(test_grantor())
1773 .sign(&key)
1774 .unwrap();
1775
1776 assert!(cap.revoked_at().is_none());
1777 let before = chrono::Utc::now().timestamp_millis();
1778 cap.revoke("test");
1779 let after = chrono::Utc::now().timestamp_millis();
1780
1781 let ts = cap.revoked_at().expect("revoked_at should be set");
1782 assert!(ts >= before && ts <= after);
1783 }
1784
1785 #[test]
1786 fn capability_revocation_reason_preserved() {
1787 let key = SecretKey::generate();
1788 let mut cap = Capability::builder()
1789 .kind(CapabilityKind::Write)
1790 .scope(ResourceScope::all())
1791 .grantor(test_grantor())
1792 .sign(&key)
1793 .unwrap();
1794
1795 cap.revoke("agent exceeded spending limit");
1796 assert_eq!(
1797 cap.revocation_reason(),
1798 Some("agent exceeded spending limit")
1799 );
1800 }
1801
1802 #[test]
1803 fn capability_lifecycle_states() {
1804 let key = SecretKey::generate();
1805 let now = chrono::Utc::now().timestamp_millis();
1806
1807 let cap = Capability::builder()
1809 .kind(CapabilityKind::Read)
1810 .scope(ResourceScope::all())
1811 .grantor(test_grantor())
1812 .expires_at(now + 60_000) .sign(&key)
1814 .unwrap();
1815 assert_eq!(cap.lifecycle_state(now), CapabilityState::Active);
1816
1817 assert_eq!(cap.lifecycle_state(now + 120_000), CapabilityState::Expired);
1819
1820 let mut cap2 = Capability::builder()
1822 .kind(CapabilityKind::Read)
1823 .scope(ResourceScope::all())
1824 .grantor(test_grantor())
1825 .expires_at(now + 60_000)
1826 .sign(&key)
1827 .unwrap();
1828 cap2.revoke("security incident");
1829 assert_eq!(cap2.lifecycle_state(now), CapabilityState::Revoked);
1830 }
1831
1832 #[test]
1833 fn capability_revoked_is_invalid() {
1834 let key = SecretKey::generate();
1835 let now = chrono::Utc::now().timestamp_millis();
1836 let mut cap = Capability::builder()
1837 .kind(CapabilityKind::Read)
1838 .scope(ResourceScope::all())
1839 .grantor(test_grantor())
1840 .expires_at(now + 60_000)
1841 .sign(&key)
1842 .unwrap();
1843
1844 assert!(cap.is_valid_at(now));
1845 cap.revoke("compromised");
1846 assert!(!cap.is_valid_at(now));
1847 }
1848
1849 #[test]
1852 fn delegate_creates_child_capability() {
1853 let key = SecretKey::generate();
1854 let cap = Capability::builder()
1855 .kind(CapabilityKind::Read)
1856 .scope(ResourceScope::pattern("repository:org/*"))
1857 .grantor(test_grantor())
1858 .delegatable(3)
1859 .sign(&key)
1860 .unwrap();
1861
1862 let child = cap.delegate(&key, None, None).unwrap();
1863
1864 assert_eq!(child.delegation_depth(), 1);
1865 assert_eq!(child.parent_capability_id(), Some(cap.id()));
1866 assert!(child.kind().matches(&CapabilityKind::Read));
1867 assert_ne!(child.id(), cap.id());
1868 }
1869
1870 #[test]
1871 fn delegate_rejects_exceeding_max_depth() {
1872 let key = SecretKey::generate();
1873 let cap = Capability::builder()
1875 .kind(CapabilityKind::Read)
1876 .scope(ResourceScope::all())
1877 .grantor(test_grantor())
1878 .delegatable(1)
1879 .sign(&key)
1880 .unwrap();
1881
1882 let child = cap.delegate(&key, None, None).unwrap();
1884 assert_eq!(child.delegation_depth(), 1);
1885
1886 let err = child.delegate(&key, None, None).unwrap_err();
1888 assert!(err.to_string().contains("delegation depth"));
1889 }
1890
1891 #[test]
1892 fn delegate_scope_must_be_subset() {
1893 let key = SecretKey::generate();
1894 let cap = Capability::builder()
1895 .kind(CapabilityKind::Read)
1896 .scope(ResourceScope::pattern("repository:org/*"))
1897 .grantor(test_grantor())
1898 .delegatable(3)
1899 .sign(&key)
1900 .unwrap();
1901
1902 let child = cap.delegate(
1904 &key,
1905 Some(ResourceScope::specific("repository:org/project")),
1906 None,
1907 );
1908 assert!(child.is_ok());
1909
1910 let err = cap
1912 .delegate(&key, Some(ResourceScope::all()), None)
1913 .unwrap_err();
1914 assert!(err.to_string().contains("subset"));
1915 }
1916
1917 #[test]
1918 fn delegate_expiry_must_not_exceed_parent() {
1919 let key = SecretKey::generate();
1920 let now = chrono::Utc::now().timestamp_millis();
1921 let cap = Capability::builder()
1922 .kind(CapabilityKind::Read)
1923 .scope(ResourceScope::all())
1924 .grantor(test_grantor())
1925 .delegatable(3)
1926 .expires_at(now + 10_000) .sign(&key)
1928 .unwrap();
1929
1930 let err = cap
1932 .delegate(&key, None, Some(Duration::from_secs(60)))
1933 .unwrap_err();
1934 assert!(err.to_string().contains("expiry"));
1935
1936 let child = cap.delegate(&key, None, Some(Duration::from_secs(5)));
1938 assert!(child.is_ok());
1939 }
1940
1941 #[test]
1942 fn delegate_non_delegatable_capability_fails() {
1943 let key = SecretKey::generate();
1944 let cap = Capability::builder()
1946 .kind(CapabilityKind::Read)
1947 .scope(ResourceScope::all())
1948 .grantor(test_grantor())
1949 .sign(&key)
1950 .unwrap();
1951
1952 assert!(!cap.is_delegatable());
1953 let err = cap.delegate(&key, None, None).unwrap_err();
1954 assert!(err.to_string().contains("not delegatable"));
1955 }
1956
1957 #[test]
1958 fn is_scope_subset_correctness() {
1959 assert!(Capability::is_scope_subset(
1961 &ResourceScope::All,
1962 &ResourceScope::All
1963 ));
1964
1965 assert!(!Capability::is_scope_subset(
1967 &ResourceScope::All,
1968 &ResourceScope::pattern("repo:*")
1969 ));
1970
1971 assert!(Capability::is_scope_subset(
1973 &ResourceScope::specific("repo:a"),
1974 &ResourceScope::All
1975 ));
1976
1977 assert!(Capability::is_scope_subset(
1979 &ResourceScope::specific("repo:org/project"),
1980 &ResourceScope::pattern("repo:org/*")
1981 ));
1982
1983 assert!(!Capability::is_scope_subset(
1985 &ResourceScope::specific("repo:other/project"),
1986 &ResourceScope::pattern("repo:org/*")
1987 ));
1988
1989 assert!(Capability::is_scope_subset(
1991 &ResourceScope::specific("repo:a"),
1992 &ResourceScope::specific("repo:a")
1993 ));
1994
1995 assert!(!Capability::is_scope_subset(
1997 &ResourceScope::specific("repo:a"),
1998 &ResourceScope::specific("repo:b")
1999 ));
2000 }
2001
2002 #[test]
2003 fn capability_set_denies_revoked() {
2004 let key = SecretKey::generate();
2005 let grantor = test_grantor();
2006
2007 let mut cap = Capability::builder()
2008 .kind(CapabilityKind::Read)
2009 .scope(ResourceScope::all())
2010 .grantor(grantor.clone())
2011 .sign(&key)
2012 .unwrap();
2013
2014 cap.revoke("policy violation");
2015
2016 let set = CapabilitySet::with_capabilities(key.public_key(), vec![cap]);
2017 let resource = test_resource("repository", "org/project");
2018 let now = chrono::Utc::now().timestamp_millis();
2019
2020 let result = set.permits(&CapabilityKind::Read, &resource, now);
2021 assert_eq!(
2022 result,
2023 CapabilityCheck::Denied {
2024 reason: DenialReason::Revoked
2025 }
2026 );
2027 }
2028
2029 #[test]
2030 fn delegate_multi_level_chain() {
2031 let key = SecretKey::generate();
2032 let cap = Capability::builder()
2033 .kind(CapabilityKind::Read)
2034 .scope(ResourceScope::all())
2035 .grantor(test_grantor())
2036 .delegatable(3)
2037 .sign(&key)
2038 .unwrap();
2039
2040 let child1 = cap.delegate(&key, None, None).unwrap();
2042 assert_eq!(child1.delegation_depth(), 1);
2043 assert_eq!(child1.parent_capability_id(), Some(cap.id()));
2044
2045 let child2 = child1.delegate(&key, None, None).unwrap();
2047 assert_eq!(child2.delegation_depth(), 2);
2048 assert_eq!(child2.parent_capability_id(), Some(child1.id()));
2049
2050 let child3 = child2.delegate(&key, None, None).unwrap();
2052 assert_eq!(child3.delegation_depth(), 3);
2053 assert_eq!(child3.parent_capability_id(), Some(child2.id()));
2054
2055 let err = child3.delegate(&key, None, None).unwrap_err();
2057 assert!(err.to_string().contains("delegation depth"));
2058 }
2059
2060 #[test]
2061 fn revoke_idempotent() {
2062 let key = SecretKey::generate();
2063 let mut cap = Capability::builder()
2064 .kind(CapabilityKind::Read)
2065 .scope(ResourceScope::all())
2066 .grantor(test_grantor())
2067 .sign(&key)
2068 .unwrap();
2069
2070 cap.revoke("first reason");
2071 let ts1 = cap.revoked_at().unwrap();
2072 let reason1 = cap.revocation_reason().unwrap().to_string();
2073
2074 std::thread::sleep(std::time::Duration::from_millis(2));
2076 cap.revoke("updated reason");
2077 let ts2 = cap.revoked_at().unwrap();
2078 let reason2 = cap.revocation_reason().unwrap().to_string();
2079
2080 assert!(cap.is_revoked());
2082 assert!(ts2 >= ts1);
2083 assert_eq!(reason2, "updated reason");
2084 assert_ne!(reason1, reason2);
2085 }
2086
2087 #[test]
2088 fn lifecycle_state_revoked_takes_precedence_over_expired() {
2089 let key = SecretKey::generate();
2090 let now = chrono::Utc::now().timestamp_millis();
2091
2092 let mut cap = Capability::builder()
2093 .kind(CapabilityKind::Read)
2094 .scope(ResourceScope::all())
2095 .grantor(test_grantor())
2096 .expires_at(now - 1000) .sign(&key)
2098 .unwrap();
2099
2100 assert_eq!(cap.lifecycle_state(now), CapabilityState::Expired);
2102
2103 cap.revoke("also revoked");
2105 assert_eq!(cap.lifecycle_state(now), CapabilityState::Revoked);
2106 }
2107
2108 #[test]
2109 fn delegate_inherits_scope_when_none() {
2110 let key = SecretKey::generate();
2111 let parent_scope = ResourceScope::pattern("repository:org/*");
2112 let cap = Capability::builder()
2113 .kind(CapabilityKind::Read)
2114 .scope(parent_scope.clone())
2115 .grantor(test_grantor())
2116 .delegatable(3)
2117 .sign(&key)
2118 .unwrap();
2119
2120 let child = cap.delegate(&key, None, None).unwrap();
2121 assert_eq!(child.scope(), &parent_scope);
2122 }
2123
2124 #[test]
2125 fn delegate_inherits_expiry_when_none() {
2126 let key = SecretKey::generate();
2127 let now = chrono::Utc::now().timestamp_millis();
2128 let parent_expiry = now + 60_000;
2129
2130 let cap = Capability::builder()
2131 .kind(CapabilityKind::Read)
2132 .scope(ResourceScope::all())
2133 .grantor(test_grantor())
2134 .delegatable(3)
2135 .expires_at(parent_expiry)
2136 .sign(&key)
2137 .unwrap();
2138
2139 let child = cap.delegate(&key, None, None).unwrap();
2140 assert_eq!(child.expires_at(), Some(parent_expiry));
2141 }
2142}