Skip to main content

wyrd_runtime/
permission.rs

1//! Runtime RBAC permission model.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7pub use wyrd_spec::auth::{
8    BifrostPermissionScope, BifrostSchemaScope, BifrostTableScope, GatewayAccess, PermissionScope,
9    PermissionScopeError,
10};
11use wyrd_spec::ids::CardUid;
12
13/// One operation/object authorization triple.
14///
15/// Wyrd RBAC is `(resource, action, scope)`: the first two axes name the
16/// operation and the third names the objects it reaches. Scope is required, so
17/// a grant that targets no particular object states [`PermissionScope::All`]
18/// explicitly rather than leaving the object axis unsaid.
19#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(try_from = "PermissionWire")]
21pub struct Permission {
22    /// Resource the permission applies to.
23    pub resource: Resource,
24    /// Action allowed on the resource.
25    pub action: Action,
26    /// Objects of that resource the permission reaches.
27    pub scope: PermissionScope,
28}
29
30/// Exact persisted and wire projection of one permission, before validation.
31///
32/// [`Permission`] deserializes through this shape so every decode — role JSON
33/// in `wyrd.auth_roles.permissions`, a request body, a signed forwarding
34/// envelope — runs the same validation. Scope has no default: a two-field
35/// object is rejected rather than silently promoted to
36/// [`PermissionScope::All`].
37#[derive(Deserialize)]
38#[serde(deny_unknown_fields)]
39struct PermissionWire {
40    /// Resource the permission applies to.
41    resource: Resource,
42    /// Action allowed on the resource.
43    action: Action,
44    /// Objects of that resource the permission reaches.
45    scope: PermissionScope,
46}
47
48impl TryFrom<PermissionWire> for Permission {
49    type Error = PermissionScopeError;
50
51    /// Validates one decoded permission before it becomes an effective grant.
52    ///
53    /// # Errors
54    ///
55    /// Returns the scope's own identity failure, or
56    /// [`PermissionScopeError::InvalidIdentifier`] with field `resource` when a
57    /// Bifrost object scope is attached to a resource that owns no Bifrost
58    /// object.
59    fn try_from(wire: PermissionWire) -> Result<Self, Self::Error> {
60        let permission = Self {
61            resource: wire.resource,
62            action: wire.action,
63            scope: wire.scope,
64        };
65        permission.validate()?;
66        Ok(permission)
67    }
68}
69
70/// Resource the permission applies to.
71#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum Resource {
74    /// Cards and specs.
75    Cards,
76    /// Service cards and deployment-facing service operations.
77    Services,
78    /// Operator cards and invocations.
79    Operators,
80    /// Eval-backed Verifier Cards and eval runs.
81    Evals,
82    /// Explicit invocation of a Verifier Card, optionally scoped to one UID.
83    Verifier,
84    /// Drift-backed Verifier Cards and drift observations.
85    Drift,
86    /// Artifact bytes and metadata.
87    Artifacts,
88    /// Audit records and audit cards.
89    Audit,
90    /// Policy cards and policy administration.
91    Policy,
92    /// Trigger cards.
93    Triggers,
94    /// Service and agent API-key principals.
95    ServiceAccounts,
96    /// Human user administration.
97    Users,
98    /// The tenant's human OIDC login connection: staging, testing,
99    /// activating, deactivating, and removing the provider its people sign in
100    /// through.
101    ///
102    /// Separate from [`Resource::ServiceAccounts`] so that holding credential
103    /// administration alone never confers authority over who may sign in to
104    /// the tenant. The built-in `admin` role reaches it through its wildcard;
105    /// no other built-in role grants it.
106    IdentityConnections,
107    /// The tenant directory itself: creating, inspecting, suspending, and
108    /// recovering administration for a tenant.
109    ///
110    /// This is a platform-control-plane resource. It governs the lifecycle of a
111    /// tenant, never the resources inside one, so holding it grants no access
112    /// to any tenant's data.
113    Tenants,
114    /// The deployment's platform-scope human identity: its one OIDC connection
115    /// and the human platform principals registered against it.
116    ///
117    /// A platform-control-plane resource, distinct from [`Resource::Tenants`]
118    /// so that administering who may sign in to the platform is visible in a
119    /// grant and in audit separately from administering tenants. Holding it
120    /// grants no access to any tenant's data.
121    PlatformIdentity,
122    /// Platform-scope credentials: the secrets that authenticate a platform
123    /// principal to the control plane.
124    ///
125    /// Separate from [`Resource::PlatformIdentity`] because the two answer
126    /// different questions. Identity administration decides *who* the platform
127    /// recognizes; credential administration mints and retires the secrets that
128    /// prove it. An operator may need to rotate the deployment's root
129    /// credential without also being able to register new administrators.
130    PlatformCredentials,
131    /// Bifrost table definitions (DDL: create/list/describe).
132    BifrostTable,
133    /// Bifrost record ingest (the streaming write surface).
134    BifrostRecord,
135    /// Bifrost table reads (SQL/scan).
136    BifrostQuery,
137    /// Private Bifrost peer plane: reservation, execution, tail, and lifecycle.
138    ///
139    /// Role-neutral on purpose. Every peer-bearing target answers the same
140    /// private services, so a Scribe and an Oracle are authorized by the same
141    /// permission rather than by role-specific resources.
142    BifrostPeer,
143    /// Gateway administration (provider credentials, deployments, policies)
144    /// and deployment/model invocation.
145    Gateway,
146    /// Captured gateway request and response payload fields.
147    GatewayPayload,
148    /// One of several resources.
149    AnyOf(Vec<Resource>),
150    /// All resources.
151    Wildcard,
152}
153
154/// Action performed against a resource.
155#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum Action {
158    /// Read.
159    Read,
160    /// Create or update.
161    Write,
162    /// Delete.
163    Delete,
164    /// Invoke runtime behavior.
165    Invoke,
166    /// Install or deploy.
167    Install,
168    /// Lock.
169    Lock,
170    /// Run.
171    Run,
172    /// Suspend a resource without destroying it, and resume it.
173    Suspend,
174    /// Restore administrative access a tenant can no longer reach itself.
175    ///
176    /// Named separately from [`Self::Write`] so the one operation that lets the
177    /// platform plane act on a tenant's administration is distinguishable in a
178    /// grant and in audit, rather than hiding inside a general write.
179    Recover,
180    /// One of several actions.
181    AnyOf(Vec<Action>),
182    /// All actions.
183    Wildcard,
184}
185
186impl Resource {
187    /// True if this resource covers `other`.
188    #[must_use]
189    pub fn covers(&self, other: &Resource) -> bool {
190        match (self, other) {
191            (Self::Wildcard, _) => true,
192            (Self::AnyOf(resources), other) => {
193                resources.iter().any(|resource| resource.covers(other))
194            }
195            (self_resource, other_resource) => self_resource == other_resource,
196        }
197    }
198
199    /// The single resources this one names: an `AnyOf` flattened, anything
200    /// else itself. Used to meet two grants member by member.
201    fn members(&self) -> Vec<&Self> {
202        match self {
203            Self::AnyOf(resources) => resources.iter().flat_map(Self::members).collect(),
204            resource => vec![resource],
205        }
206    }
207
208    /// True when this resource owns Bifrost objects a grant may be scoped to.
209    ///
210    /// Bifrost query reads and record writes name a table. `AnyOf` and
211    /// `Wildcard` are deliberately excluded: a multi-resource or wildcard grant
212    /// reaches objects only through [`PermissionScope::All`].
213    #[must_use]
214    pub const fn accepts_bifrost_scope(&self) -> bool {
215        matches!(self, Self::BifrostQuery | Self::BifrostRecord)
216    }
217
218    /// Stable label for this resource, or `None` for a composite `AnyOf`.
219    ///
220    /// Audit records the label rather than the enum's debug shape so a stored
221    /// decision stays readable and comparable across releases.
222    #[must_use]
223    pub fn as_str(&self) -> Option<&'static str> {
224        Some(match self {
225            Self::Cards => "cards",
226            Self::Services => "services",
227            Self::Operators => "operators",
228            Self::Evals => "evals",
229            Self::Verifier => "verifier",
230            Self::Drift => "drift",
231            Self::Artifacts => "artifacts",
232            Self::Audit => "audit",
233            Self::Policy => "policy",
234            Self::Triggers => "triggers",
235            Self::ServiceAccounts => "service_accounts",
236            Self::Users => "users",
237            Self::IdentityConnections => "identity_connections",
238            Self::Tenants => "tenants",
239            Self::PlatformIdentity => "platform_identity",
240            Self::PlatformCredentials => "platform_credentials",
241            Self::BifrostTable => "bifrost_table",
242            Self::BifrostRecord => "bifrost_record",
243            Self::BifrostQuery => "bifrost_query",
244            Self::BifrostPeer => "bifrost_peer",
245            Self::Gateway => "gateway",
246            Self::GatewayPayload => "gateway_payload",
247            Self::Wildcard => "wildcard",
248            Self::AnyOf(_) => return None,
249        })
250    }
251}
252
253impl Action {
254    /// True if this action covers `other`.
255    #[must_use]
256    pub fn covers(&self, other: &Action) -> bool {
257        match (self, other) {
258            (Self::Wildcard, _) => true,
259            (Self::AnyOf(actions), other) => actions.iter().any(|action| action.covers(other)),
260            (self_action, other_action) => self_action == other_action,
261        }
262    }
263
264    /// The single actions this one names: an `AnyOf` flattened, anything else
265    /// itself. Used to meet two grants member by member.
266    fn members(&self) -> Vec<&Self> {
267        match self {
268            Self::AnyOf(actions) => actions.iter().flat_map(Self::members).collect(),
269            action => vec![action],
270        }
271    }
272
273    /// Stable label for this action, or `None` for a composite `AnyOf`.
274    ///
275    /// Audit records the label rather than the enum's debug shape so a stored
276    /// decision stays readable and comparable across releases.
277    #[must_use]
278    pub fn as_str(&self) -> Option<&'static str> {
279        Some(match self {
280            Self::Read => "read",
281            Self::Write => "write",
282            Self::Delete => "delete",
283            Self::Invoke => "invoke",
284            Self::Install => "install",
285            Self::Lock => "lock",
286            Self::Run => "run",
287            Self::Suspend => "suspend",
288            Self::Recover => "recover",
289            Self::Wildcard => "wildcard",
290            Self::AnyOf(_) => return None,
291        })
292    }
293}
294
295impl Permission {
296    /// True if this permission covers `required`.
297    #[must_use]
298    pub fn covers(&self, required: &Permission) -> bool {
299        self.resource.covers(&required.resource)
300            && self.action.covers(&required.action)
301            && self.scope.covers(&required.scope)
302    }
303
304    /// True when holding `self` makes `other` redundant in a [`PermissionSet`].
305    ///
306    /// Subsumption is [`Self::covers`] except that an `all`-scoped grant never
307    /// subsumes an explicit gateway grant: `all` reaches only `open`
308    /// deployments, while the explicit grant is the only one reaching a
309    /// `granted` deployment. Requirement matching still uses `covers`.
310    #[must_use]
311    pub fn subsumes(&self, other: &Permission) -> bool {
312        self.covers(other) && !(self.scope == PermissionScope::All && other.scope.is_gateway())
313    }
314
315    /// Rejects a permission whose object scope cannot apply to its operation.
316    ///
317    /// Bifrost object scope is meaningful only for Bifrost query reads and
318    /// record writes, gateway object scope only for gateway invocation, and
319    /// Verifier object scope only for `verifier:run`. A wildcard or
320    /// multi-resource grant on either axis keeps object-wide reach only with
321    /// [`PermissionScope::All`], which is what stops a wildcard from inheriting
322    /// one object's narrow authority.
323    ///
324    /// # Errors
325    ///
326    /// Returns the scope's own identity failure, or
327    /// [`PermissionScopeError::InvalidIdentifier`] with field `resource` when an
328    /// object scope is attached to a resource that does not own that object, or
329    /// with field `action` when a Bifrost scope is attached to an action other
330    /// than [`Action::Read`] for query or [`Action::Write`] for record, a
331    /// gateway scope to any action other than [`Action::Invoke`], or a Verifier
332    /// scope to any action other than [`Action::Run`].
333    pub fn validate(&self) -> Result<(), PermissionScopeError> {
334        self.scope.validate()?;
335        let (resource_accepts, required_action) = match &self.scope {
336            PermissionScope::All => return Ok(()),
337            PermissionScope::Bifrost(_) => (
338                self.resource.accepts_bifrost_scope(),
339                if self.resource == Resource::BifrostRecord {
340                    Action::Write
341                } else {
342                    Action::Read
343                },
344            ),
345            PermissionScope::Gateway(_) => (self.resource == Resource::Gateway, Action::Invoke),
346            PermissionScope::Verifier(_) => (self.resource == Resource::Verifier, Action::Run),
347        };
348        if !resource_accepts {
349            return Err(PermissionScopeError::InvalidIdentifier {
350                field: "resource",
351                value: self.resource.as_str().unwrap_or("any_of").to_owned(),
352            });
353        }
354        if self.action != required_action {
355            return Err(PermissionScopeError::InvalidIdentifier {
356                field: "action",
357                value: self.action.as_str().unwrap_or("any_of").to_owned(),
358            });
359        }
360        Ok(())
361    }
362
363    /// Read cards.
364    #[must_use]
365    pub const fn card_read() -> Self {
366        Self {
367            resource: Resource::Cards,
368            action: Action::Read,
369            scope: PermissionScope::All,
370        }
371    }
372
373    /// Write cards.
374    #[must_use]
375    pub const fn card_write() -> Self {
376        Self {
377            resource: Resource::Cards,
378            action: Action::Write,
379            scope: PermissionScope::All,
380        }
381    }
382
383    /// Delete cards.
384    #[must_use]
385    pub const fn card_delete() -> Self {
386        Self {
387            resource: Resource::Cards,
388            action: Action::Delete,
389            scope: PermissionScope::All,
390        }
391    }
392
393    /// Read artifacts.
394    #[must_use]
395    pub const fn artifact_read() -> Self {
396        Self {
397            resource: Resource::Artifacts,
398            action: Action::Read,
399            scope: PermissionScope::All,
400        }
401    }
402
403    /// Write artifacts.
404    #[must_use]
405    pub const fn artifact_write() -> Self {
406        Self {
407            resource: Resource::Artifacts,
408            action: Action::Write,
409            scope: PermissionScope::All,
410        }
411    }
412
413    /// Install services.
414    #[must_use]
415    pub const fn service_install() -> Self {
416        Self {
417            resource: Resource::Services,
418            action: Action::Install,
419            scope: PermissionScope::All,
420        }
421    }
422
423    /// Write service accounts.
424    #[must_use]
425    pub const fn service_accounts_write() -> Self {
426        Self {
427            resource: Resource::ServiceAccounts,
428            action: Action::Write,
429            scope: PermissionScope::All,
430        }
431    }
432
433    /// Administer the tenant's human OIDC login connection.
434    #[must_use]
435    pub const fn identity_connections_write() -> Self {
436        Self {
437            resource: Resource::IdentityConnections,
438            action: Action::Write,
439            scope: PermissionScope::All,
440        }
441    }
442
443    /// Read the tenant's redacted Operator connections.
444    #[must_use]
445    pub const fn operators_read() -> Self {
446        Self {
447            resource: Resource::Operators,
448            action: Action::Read,
449            scope: PermissionScope::All,
450        }
451    }
452
453    /// Create, update, rotate, and disable the tenant's Operator connections.
454    #[must_use]
455    pub const fn operators_write() -> Self {
456        Self {
457            resource: Resource::Operators,
458            action: Action::Write,
459            scope: PermissionScope::All,
460        }
461    }
462
463    /// Invoke operators.
464    #[must_use]
465    pub const fn operator_invoke() -> Self {
466        Self {
467            resource: Resource::Operators,
468            action: Action::Invoke,
469            scope: PermissionScope::All,
470        }
471    }
472
473    /// Run evals.
474    #[must_use]
475    pub const fn eval_run() -> Self {
476        Self {
477            resource: Resource::Evals,
478            action: Action::Run,
479            scope: PermissionScope::All,
480        }
481    }
482
483    /// Invoke the one Verifier Card `verifier`, directly or as a manual run.
484    ///
485    /// The permission an explicit invocation requires: only a grant of this
486    /// exact UID or [`Self::verifier_run_any`] covers it.
487    #[must_use]
488    pub const fn verifier_run(verifier: CardUid) -> Self {
489        Self {
490            resource: Resource::Verifier,
491            action: Action::Run,
492            scope: PermissionScope::Verifier(verifier),
493        }
494    }
495
496    /// Invoke every Verifier Card in the tenant.
497    ///
498    /// The unscoped grant the built-in `workload` and `editor` Roles carry; a
499    /// narrower custom Role grants [`Self::verifier_run`] for one UID.
500    #[must_use]
501    pub const fn verifier_run_any() -> Self {
502        Self {
503            resource: Resource::Verifier,
504            action: Action::Run,
505            scope: PermissionScope::All,
506        }
507    }
508
509    /// Write triggers.
510    #[must_use]
511    pub const fn trigger_write() -> Self {
512        Self {
513            resource: Resource::Triggers,
514            action: Action::Write,
515            scope: PermissionScope::All,
516        }
517    }
518
519    /// Read audit.
520    #[must_use]
521    pub const fn audit_read() -> Self {
522        Self {
523            resource: Resource::Audit,
524            action: Action::Read,
525            scope: PermissionScope::All,
526        }
527    }
528
529    /// Lock policy.
530    #[must_use]
531    pub const fn policy_lock() -> Self {
532        Self {
533            resource: Resource::Policy,
534            action: Action::Lock,
535            scope: PermissionScope::All,
536        }
537    }
538
539    /// Create a tenant on the platform control plane.
540    #[must_use]
541    pub const fn tenant_create() -> Self {
542        Self {
543            resource: Resource::Tenants,
544            action: Action::Write,
545            scope: PermissionScope::All,
546        }
547    }
548
549    /// List tenants and inspect one tenant's lifecycle state.
550    #[must_use]
551    pub const fn tenant_read() -> Self {
552        Self {
553            resource: Resource::Tenants,
554            action: Action::Read,
555            scope: PermissionScope::All,
556        }
557    }
558
559    /// Suspend and resume a tenant without destroying its state.
560    #[must_use]
561    pub const fn tenant_suspend() -> Self {
562        Self {
563            resource: Resource::Tenants,
564            action: Action::Suspend,
565            scope: PermissionScope::All,
566        }
567    }
568
569    /// Restore administrative access to a tenant that has lost its own.
570    ///
571    /// The single named capability by which the platform plane reaches a
572    /// tenant, kept distinct so it is visible in a grant and in audit rather
573    /// than hidden inside a general write.
574    #[must_use]
575    pub const fn tenant_recover_admin() -> Self {
576        Self {
577            resource: Resource::Tenants,
578            action: Action::Recover,
579            scope: PermissionScope::All,
580        }
581    }
582
583    /// Read the platform-scope identity configuration.
584    ///
585    /// Reading never yields provider secrets; the connection view is redacted
586    /// at the boundary regardless of who holds this.
587    #[must_use]
588    pub const fn platform_identity_read() -> Self {
589        Self {
590            resource: Resource::PlatformIdentity,
591            action: Action::Read,
592            scope: PermissionScope::All,
593        }
594    }
595
596    /// Configure the platform OIDC connection and register human platform
597    /// principals.
598    ///
599    /// Deliberately the same permission for both: registering a principal
600    /// against a connection you also control is one capability, and splitting
601    /// it would suggest a separation the plane does not actually have.
602    #[must_use]
603    pub const fn platform_identity_write() -> Self {
604        Self {
605            resource: Resource::PlatformIdentity,
606            action: Action::Write,
607            scope: PermissionScope::All,
608        }
609    }
610
611    /// List the non-secret metadata of a platform principal's credentials.
612    ///
613    /// Listing never yields credential material; it exists so an operator can
614    /// see what is live, expired, or already retired before rotating.
615    #[must_use]
616    pub const fn platform_credential_read() -> Self {
617        Self {
618            resource: Resource::PlatformCredentials,
619            action: Action::Read,
620            scope: PermissionScope::All,
621        }
622    }
623
624    /// Issue and revoke platform credentials.
625    ///
626    /// One permission for both halves of rotation: an operator who can mint a
627    /// replacement must be able to retire what it replaces, and splitting them
628    /// would leave a superseded credential live because the second half was
629    /// separately granted.
630    #[must_use]
631    pub const fn platform_credential_write() -> Self {
632        Self {
633            resource: Resource::PlatformCredentials,
634            action: Action::Write,
635            scope: PermissionScope::All,
636        }
637    }
638
639    /// Manage users.
640    #[must_use]
641    pub const fn users_manage() -> Self {
642        Self {
643            resource: Resource::Users,
644            action: Action::Write,
645            scope: PermissionScope::All,
646        }
647    }
648
649    /// Write Bifrost records (the ingest surface).
650    #[must_use]
651    pub const fn bifrost_record_write() -> Self {
652        Self {
653            resource: Resource::BifrostRecord,
654            action: Action::Write,
655            scope: PermissionScope::All,
656        }
657    }
658
659    /// Invoke the private Bifrost peer protocol.
660    #[must_use]
661    pub const fn bifrost_peer_invoke() -> Self {
662        Self {
663            resource: Resource::BifrostPeer,
664            action: Action::Invoke,
665            scope: PermissionScope::All,
666        }
667    }
668
669    /// Read Bifrost table definitions.
670    #[must_use]
671    pub const fn bifrost_table_read() -> Self {
672        Self {
673            resource: Resource::BifrostTable,
674            action: Action::Read,
675            scope: PermissionScope::All,
676        }
677    }
678
679    /// Write Bifrost table definitions (DDL).
680    #[must_use]
681    pub const fn bifrost_table_write() -> Self {
682        Self {
683            resource: Resource::BifrostTable,
684            action: Action::Write,
685            scope: PermissionScope::All,
686        }
687    }
688
689    /// Install a `SystemShared` Bifrost table (cross-tenant DDL).
690    #[must_use]
691    pub const fn bifrost_table_install() -> Self {
692        Self {
693            resource: Resource::BifrostTable,
694            action: Action::Install,
695            scope: PermissionScope::All,
696        }
697    }
698
699    /// Read Bifrost tables (SQL/scan).
700    #[must_use]
701    pub const fn bifrost_query_read() -> Self {
702        Self {
703            resource: Resource::BifrostQuery,
704            action: Action::Read,
705            scope: PermissionScope::All,
706        }
707    }
708
709    /// Read gateway deployments, redacted credential metadata, and policies.
710    #[must_use]
711    pub const fn gateway_read() -> Self {
712        Self {
713            resource: Resource::Gateway,
714            action: Action::Read,
715            scope: PermissionScope::All,
716        }
717    }
718
719    /// Create, replace, rotate, or revoke gateway configuration and policies.
720    #[must_use]
721    pub const fn gateway_write() -> Self {
722        Self {
723            resource: Resource::Gateway,
724            action: Action::Write,
725            scope: PermissionScope::All,
726        }
727    }
728
729    /// Delete gateway credentials, deployments, and policies.
730    #[must_use]
731    pub const fn gateway_delete() -> Self {
732        Self {
733            resource: Resource::Gateway,
734            action: Action::Delete,
735            scope: PermissionScope::All,
736        }
737    }
738
739    /// Invoke the gateway for the deployment or model named by `access`.
740    #[must_use]
741    pub const fn gateway_invoke(access: GatewayAccess) -> Self {
742        Self {
743            resource: Resource::Gateway,
744            action: Action::Invoke,
745            scope: PermissionScope::Gateway(access),
746        }
747    }
748
749    /// Invoke the gateway for every `open` deployment and its models.
750    ///
751    /// The unscoped grant the built-in `workload` Role carries; a narrower
752    /// custom Role uses [`Self::gateway_invoke`] with one deployment or model,
753    /// which is also the only grant reaching a `granted` deployment.
754    #[must_use]
755    pub const fn gateway_invoke_any() -> Self {
756        Self {
757            resource: Resource::Gateway,
758            action: Action::Invoke,
759            scope: PermissionScope::All,
760        }
761    }
762
763    /// Read captured gateway payload fields.
764    #[must_use]
765    pub const fn gateway_payload_read() -> Self {
766        Self {
767            resource: Resource::GatewayPayload,
768            action: Action::Read,
769            scope: PermissionScope::All,
770        }
771    }
772
773    /// All permissions.
774    #[must_use]
775    pub const fn wildcard() -> Self {
776        Self {
777            resource: Resource::Wildcard,
778            action: Action::Wildcard,
779            scope: PermissionScope::All,
780        }
781    }
782}
783
784/// Renders the permission's *operation* as `resource:action`.
785///
786/// The object axis is deliberately absent: this string names which operation
787/// was exercised for audit text and diagnostics, and is never the authorization
788/// decision. Scoped authority travels as the typed [`Permission`] and, for
789/// distributed execution, inside the scoped permission digest.
790impl fmt::Display for Permission {
791    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792        let resource = self.resource.as_str().ok_or(fmt::Error)?;
793        let action = self.action.as_str().ok_or(fmt::Error)?;
794        write!(f, "{resource}:{action}")
795    }
796}
797
798/// Permission parse failure.
799#[derive(Debug, thiserror::Error)]
800#[error("permission must be formatted as resource:action")]
801pub struct PermissionParseError;
802
803/// Parses one `resource:action` operation token into an [`PermissionScope::All`]
804/// permission.
805///
806/// The token language carries no object, so a parsed permission is the
807/// object-wide form. Scoped grants are expressed only through the typed JSON
808/// projection; there is no second string spelling for an object.
809impl FromStr for Permission {
810    type Err = PermissionParseError;
811
812    fn from_str(value: &str) -> Result<Self, Self::Err> {
813        let Some((resource, action)) = value.split_once(':') else {
814            return Err(PermissionParseError);
815        };
816        Ok(Self {
817            resource: parse_resource(resource)?,
818            action: parse_action(action)?,
819            scope: PermissionScope::All,
820        })
821    }
822}
823
824fn parse_resource(value: &str) -> Result<Resource, PermissionParseError> {
825    Ok(match value {
826        "cards" => Resource::Cards,
827        "services" => Resource::Services,
828        "operators" => Resource::Operators,
829        "evals" => Resource::Evals,
830        "verifier" => Resource::Verifier,
831        "drift" => Resource::Drift,
832        "artifacts" => Resource::Artifacts,
833        "audit" => Resource::Audit,
834        "policy" => Resource::Policy,
835        "triggers" => Resource::Triggers,
836        "service_accounts" => Resource::ServiceAccounts,
837        "users" => Resource::Users,
838        "identity_connections" => Resource::IdentityConnections,
839        "bifrost_table" => Resource::BifrostTable,
840        "bifrost_record" => Resource::BifrostRecord,
841        "bifrost_peer" => Resource::BifrostPeer,
842        "bifrost_query" => Resource::BifrostQuery,
843        "gateway" => Resource::Gateway,
844        "gateway_payload" => Resource::GatewayPayload,
845        "wildcard" => Resource::Wildcard,
846        _ => return Err(PermissionParseError),
847    })
848}
849
850fn parse_action(value: &str) -> Result<Action, PermissionParseError> {
851    Ok(match value {
852        "read" => Action::Read,
853        "write" => Action::Write,
854        "delete" => Action::Delete,
855        "invoke" => Action::Invoke,
856        "install" => Action::Install,
857        "lock" => Action::Lock,
858        "run" => Action::Run,
859        "wildcard" => Action::Wildcard,
860        _ => return Err(PermissionParseError),
861    })
862}
863
864/// Two permission sets whose exact shared authority no permission can state.
865///
866/// Delegation refuses on this rather than signing a token that widens either
867/// party or drops authority both hold.
868#[derive(Debug, thiserror::Error)]
869#[error("shared authority for {permission} depends on deployment access and cannot be represented")]
870pub struct UnrepresentableIntersection {
871    /// The gateway grant one side holds only through `all`.
872    pub permission: Permission,
873}
874
875/// Subsumption-aware permission collection.
876#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
877#[serde(transparent)]
878pub struct PermissionSet(Vec<Permission>);
879
880impl PermissionSet {
881    /// Construct an empty set.
882    #[must_use]
883    pub fn new() -> Self {
884        Self(Vec::new())
885    }
886
887    /// Insert one permission while keeping the set minimal.
888    ///
889    /// A grant is dropped only when another grant [`Permission::subsumes`] it,
890    /// so an explicit gateway grant survives beside `all` in either order.
891    pub fn insert(&mut self, permission: Permission) {
892        if self.0.iter().any(|existing| existing.subsumes(&permission)) {
893            return;
894        }
895        self.0.retain(|existing| !permission.subsumes(existing));
896        self.0.push(permission);
897    }
898
899    /// True if the set covers `required`.
900    #[must_use]
901    pub fn contains(&self, required: &Permission) -> bool {
902        self.0.iter().any(|permission| permission.covers(required))
903    }
904
905    /// True if a grant naming specific objects, not [`PermissionScope::All`],
906    /// covers `required`.
907    ///
908    /// A `granted` gateway deployment admits only such a grant, so the
909    /// unscoped invoke grant of the built-in Roles never reaches it.
910    #[must_use]
911    pub fn contains_object_scoped(&self, required: &Permission) -> bool {
912        self.0.iter().any(|permission| {
913            permission.scope != PermissionScope::All && permission.covers(required)
914        })
915    }
916
917    /// True when the set grants `action` on `resource` under *any* object scope.
918    ///
919    /// This is coarse admission, not authorization: it answers "does this
920    /// principal hold this capability at all", which is what a public route
921    /// needs before it can resolve the objects a request actually touches. The
922    /// authoritative decision is [`Self::contains`] against the resolved
923    /// object, and no caller may substitute this for it.
924    #[must_use]
925    pub fn covers_operation(&self, resource: &Resource, action: &Action) -> bool {
926        self.0.iter().any(|permission| {
927            permission.resource.covers(resource) && permission.action.covers(action)
928        })
929    }
930
931    /// The authority both sets grant: every permission covered by `self` and by
932    /// `other`, and nothing else.
933    ///
934    /// Delegation mints with this so a delegated token can never carry more
935    /// than its caller holds. Each pair of grants meets axis by axis through the
936    /// same `covers` relations authorization uses: an `AnyOf` is split into its
937    /// members, each axis keeps the narrower side when one covers the other, and
938    /// a pair with a disjoint axis contributes nothing. A wildcard therefore
939    /// yields the other grant, and a schema grant meeting a table grant in that
940    /// schema yields the table grant.
941    ///
942    /// The one meet no scope can state is `all` against an explicit gateway
943    /// grant: `all` reaches only `open` deployments, so their shared authority
944    /// is "that deployment, only while it is open". Such a meet is accepted only
945    /// when the rest of the result already covers it — both sides also hold
946    /// `all`, or both grant the deployment explicitly; otherwise the complete
947    /// intersection is unrepresentable.
948    ///
949    /// # Errors
950    ///
951    /// Returns [`UnrepresentableIntersection`] naming the first gateway grant
952    /// whose shared authority depends on deployment access, so delegation can
953    /// refuse instead of widening or silently dropping it.
954    pub fn intersection(&self, other: &Self) -> Result<Self, UnrepresentableIntersection> {
955        let mut meet = Self::new();
956        let mut open_only = Vec::new();
957        for left in &self.0 {
958            for right in &other.0 {
959                let Some(scope) = narrower(&left.scope, &right.scope, PermissionScope::covers)
960                else {
961                    continue;
962                };
963                let conditional = scope.is_gateway()
964                    && (left.scope == PermissionScope::All || right.scope == PermissionScope::All);
965                for resource in meets(
966                    &left.resource.members(),
967                    &right.resource.members(),
968                    Resource::covers,
969                ) {
970                    for action in meets(
971                        &left.action.members(),
972                        &right.action.members(),
973                        Action::covers,
974                    ) {
975                        let permission = Permission {
976                            resource: resource.clone(),
977                            action: action.clone(),
978                            scope: scope.clone(),
979                        };
980                        if permission.validate().is_err() {
981                            continue;
982                        }
983                        if conditional {
984                            open_only.push(permission);
985                        } else {
986                            meet.insert(permission);
987                        }
988                    }
989                }
990            }
991        }
992        match open_only
993            .into_iter()
994            .find(|permission| !meet.contains(permission))
995        {
996            Some(permission) => Err(UnrepresentableIntersection { permission }),
997            None => Ok(meet),
998        }
999    }
1000
1001    /// Iterate over stored permissions.
1002    pub fn iter(&self) -> impl Iterator<Item = &Permission> {
1003        self.0.iter()
1004    }
1005
1006    /// Number of stored permissions.
1007    #[must_use]
1008    pub fn len(&self) -> usize {
1009        self.0.len()
1010    }
1011
1012    /// True when empty.
1013    #[must_use]
1014    pub fn is_empty(&self) -> bool {
1015        self.0.is_empty()
1016    }
1017}
1018
1019/// The narrower of two values on one axis, or `None` when neither covers the
1020/// other and the axis is disjoint.
1021///
1022/// Exact for the resource, action, and scope axes because each is a tree under
1023/// its `covers` relation once `AnyOf` is split into members.
1024fn narrower<'a, T>(left: &'a T, right: &'a T, covers: fn(&T, &T) -> bool) -> Option<&'a T> {
1025    if covers(left, right) {
1026        Some(right)
1027    } else if covers(right, left) {
1028        Some(left)
1029    } else {
1030        None
1031    }
1032}
1033
1034/// Every non-empty [`narrower`] meet between two lists of axis members.
1035fn meets<'a, T>(left: &[&'a T], right: &[&'a T], covers: fn(&T, &T) -> bool) -> Vec<&'a T> {
1036    left.iter()
1037        .flat_map(|l| right.iter().filter_map(move |r| narrower(*l, *r, covers)))
1038        .collect()
1039}
1040
1041impl FromIterator<Permission> for PermissionSet {
1042    fn from_iter<T: IntoIterator<Item = Permission>>(iter: T) -> Self {
1043        let mut set = Self::new();
1044        for permission in iter {
1045            set.insert(permission);
1046        }
1047        set
1048    }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053    use super::{Action, Permission, PermissionScope, PermissionSet, Resource};
1054    use serde_json::json;
1055    use wyrd_spec::auth::{BifrostPermissionScope, BifrostSchemaScope, BifrostTableScope};
1056
1057    #[test]
1058    fn serde_round_trip_every_variant() {
1059        let resources = [
1060            Resource::Cards,
1061            Resource::Services,
1062            Resource::Operators,
1063            Resource::Evals,
1064            Resource::Verifier,
1065            Resource::Drift,
1066            Resource::Artifacts,
1067            Resource::Audit,
1068            Resource::Policy,
1069            Resource::Triggers,
1070            Resource::ServiceAccounts,
1071            Resource::Users,
1072            Resource::IdentityConnections,
1073            Resource::AnyOf(vec![Resource::Operators, Resource::Evals]),
1074            Resource::Wildcard,
1075        ];
1076        for resource in resources {
1077            let value = serde_json::to_value(&resource).expect("resource serializes");
1078            let round_trip: Resource =
1079                serde_json::from_value(value).expect("resource deserializes");
1080            assert_eq!(round_trip, resource);
1081        }
1082
1083        let actions = [
1084            Action::Read,
1085            Action::Write,
1086            Action::Delete,
1087            Action::Invoke,
1088            Action::Install,
1089            Action::Lock,
1090            Action::Run,
1091            Action::AnyOf(vec![Action::Read, Action::Write]),
1092            Action::Wildcard,
1093        ];
1094        for action in actions {
1095            let value = serde_json::to_value(&action).expect("action serializes");
1096            let round_trip: Action = serde_json::from_value(value).expect("action deserializes");
1097            assert_eq!(round_trip, action);
1098        }
1099    }
1100
1101    #[test]
1102    fn bifrost_permissions_round_trip_through_wire_strings() {
1103        for (permission, wire) in [
1104            (Permission::bifrost_record_write(), "bifrost_record:write"),
1105            (Permission::bifrost_table_read(), "bifrost_table:read"),
1106            (Permission::bifrost_table_write(), "bifrost_table:write"),
1107            (Permission::bifrost_query_read(), "bifrost_query:read"),
1108            (Permission::bifrost_peer_invoke(), "bifrost_peer:invoke"),
1109        ] {
1110            assert_eq!(permission.to_string(), wire);
1111            assert_eq!(wire.parse::<Permission>().expect("wire parses"), permission);
1112            let json = serde_json::to_value(&permission).expect("serializes");
1113            assert_eq!(
1114                serde_json::from_value::<Permission>(json).expect("deserializes"),
1115                permission
1116            );
1117        }
1118    }
1119
1120    #[test]
1121    fn bifrost_record_write_is_distinct_from_table_write() {
1122        assert!(!Permission::bifrost_table_write().covers(&Permission::bifrost_record_write()));
1123        assert!(!Permission::bifrost_record_write().covers(&Permission::bifrost_table_write()));
1124        assert!(Permission::wildcard().covers(&Permission::bifrost_record_write()));
1125    }
1126
1127    #[test]
1128    fn covers_wildcard_resource() {
1129        let permission = Permission {
1130            resource: Resource::Wildcard,
1131            action: Action::Read,
1132            scope: PermissionScope::All,
1133        };
1134
1135        assert!(permission.covers(&Permission::card_read()));
1136        assert!(!permission.covers(&Permission::card_write()));
1137    }
1138
1139    #[test]
1140    fn covers_wildcard_action() {
1141        let permission = Permission {
1142            resource: Resource::Cards,
1143            action: Action::Wildcard,
1144            scope: PermissionScope::All,
1145        };
1146
1147        assert!(permission.covers(&Permission::card_read()));
1148        assert!(permission.covers(&Permission::card_write()));
1149        assert!(!permission.covers(&Permission::artifact_write()));
1150    }
1151
1152    /// The resource-and-action wildcard covers every concrete grant, including
1153    /// Bifrost record writes outside the Card family.
1154    ///
1155    /// # Panics
1156    /// Panics when the wildcard fails to cover any probed permission.
1157    #[test]
1158    fn covers_double_wildcard() {
1159        let permission = Permission::wildcard();
1160
1161        assert!(permission.covers(&Permission::card_read()));
1162        assert!(permission.covers(&Permission::card_write()));
1163        assert!(permission.covers(&Permission::bifrost_record_write()));
1164    }
1165
1166    #[test]
1167    fn covers_anyof_resource() {
1168        let permission = Permission {
1169            resource: Resource::AnyOf(vec![Resource::Operators, Resource::Evals]),
1170            action: Action::Invoke,
1171            scope: PermissionScope::All,
1172        };
1173
1174        assert!(permission.covers(&Permission::operator_invoke()));
1175        assert!(permission.covers(&Permission {
1176            resource: Resource::Evals,
1177            action: Action::Invoke,
1178            scope: PermissionScope::All,
1179        }));
1180        assert!(!permission.covers(&Permission {
1181            resource: Resource::Cards,
1182            action: Action::Invoke,
1183            scope: PermissionScope::All,
1184        }));
1185    }
1186
1187    #[test]
1188    fn permission_set_subsumption_drops_redundant() {
1189        let mut set = PermissionSet::from_iter([Permission::card_read()]);
1190        set.insert(Permission::wildcard());
1191
1192        assert_eq!(set.len(), 1);
1193        assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1194    }
1195
1196    #[test]
1197    fn permission_set_subsumption_skips_covered() {
1198        let mut set = PermissionSet::from_iter([Permission::wildcard()]);
1199        set.insert(Permission::card_read());
1200
1201        assert_eq!(set.len(), 1);
1202        assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1203    }
1204
1205    /// Set membership resolves through coverage, so a set holding only the
1206    /// wildcard contains every concrete permission.
1207    ///
1208    /// # Panics
1209    /// Panics when the wildcard-only set does not contain a probed permission.
1210    #[test]
1211    fn permission_set_contains_resolves_through_wildcard() {
1212        let set = PermissionSet::from_iter([Permission::wildcard()]);
1213
1214        assert!(set.contains(&Permission::card_read()));
1215        assert!(set.contains(&Permission::bifrost_record_write()));
1216    }
1217
1218    #[test]
1219    fn permission_set_jsonb_round_trip() {
1220        let permissions = vec![
1221            Permission::card_write(),
1222            Permission::card_read(),
1223            Permission {
1224                resource: Resource::AnyOf(vec![Resource::Operators, Resource::Evals]),
1225                action: Action::Invoke,
1226                scope: PermissionScope::All,
1227            },
1228            Permission::wildcard(),
1229        ];
1230        let value = serde_json::to_value(&permissions).expect("permissions serialize");
1231
1232        assert_eq!(
1233            value,
1234            json!([
1235                {"resource": "cards", "action": "write", "scope": "all"},
1236                {"resource": "cards", "action": "read", "scope": "all"},
1237                {"resource": {"any_of": ["operators", "evals"]}, "action": "invoke", "scope": "all"},
1238                {"resource": "wildcard", "action": "wildcard", "scope": "all"}
1239            ])
1240        );
1241
1242        let round_trip: Vec<Permission> =
1243            serde_json::from_value(value).expect("permissions deserialize");
1244        assert_eq!(round_trip, permissions);
1245    }
1246
1247    /// Proves gateway grants round-trip their approved wire names, a
1248    /// deployment invoke grant covers that deployment's models only, only an
1249    /// object-scoped grant counts as scoped, and a gateway object scope is
1250    /// refused anywhere but gateway invoke.
1251    ///
1252    /// # Panics
1253    /// Panics when a fixture fails to build or an assertion fails.
1254    #[test]
1255    fn gateway_permissions_are_typed_and_scope_checked() {
1256        use super::GatewayAccess;
1257        use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1258        let openai = ProviderDeploymentName::new("openai").expect("valid deployment");
1259        let provider = GatewayAccess::Deployment {
1260            deployment: openai.clone(),
1261        };
1262        let model = |name: &str| GatewayAccess::Model {
1263            deployment: openai.clone(),
1264            model: ModelId::new(name).expect("valid model"),
1265        };
1266        let required = Permission::gateway_invoke(model("gpt-4o"));
1267        assert!(PermissionSet::from_iter([Permission::gateway_invoke_any()]).contains(&required));
1268        assert!(
1269            !PermissionSet::from_iter([Permission::gateway_invoke_any()])
1270                .contains_object_scoped(&required),
1271            "the unscoped grant is not a grant of the deployment"
1272        );
1273        assert!(
1274            PermissionSet::from_iter([Permission::gateway_invoke(provider.clone())])
1275                .contains_object_scoped(&required)
1276        );
1277
1278        assert_eq!(
1279            Permission::gateway_payload_read().to_string(),
1280            "gateway_payload:read"
1281        );
1282        assert_eq!(
1283            "gateway:write".parse::<Permission>().expect("parses"),
1284            Permission::gateway_write()
1285        );
1286        assert!(
1287            Permission::gateway_invoke(provider.clone())
1288                .covers(&Permission::gateway_invoke(model("gpt-4o")))
1289        );
1290        assert!(
1291            !Permission::gateway_invoke(model("gpt-4o"))
1292                .covers(&Permission::gateway_invoke(model("gpt-4.1")))
1293        );
1294        assert!(
1295            !Permission::gateway_invoke(provider.clone()).covers(&Permission::gateway_read()),
1296            "invoke never grants admin metadata reads"
1297        );
1298        assert_eq!(
1299            serde_json::to_value(Permission::gateway_invoke(model("gpt-4o"))).expect("serializes"),
1300            json!({
1301                "resource": "gateway",
1302                "action": "invoke",
1303                "scope": {"gateway": {"model": {"deployment": "openai", "model": "gpt-4o"}}}
1304            })
1305        );
1306        for (resource, action) in [("gateway", "read"), ("cards", "invoke")] {
1307            assert!(
1308                serde_json::from_value::<Permission>(json!({
1309                    "resource": resource,
1310                    "action": action,
1311                    "scope": {"gateway": {"deployment": {"deployment": "openai"}}}
1312                }))
1313                .is_err(),
1314                "{resource}:{action}"
1315            );
1316        }
1317    }
1318
1319    /// Proves `verifier:run` parses, covers only its exact Verifier UID or the
1320    /// unscoped grant, is never implied by `evals:run`, and refuses a Verifier
1321    /// scope on any other operation.
1322    #[test]
1323    fn verifier_run_is_uid_scoped_and_distinct_from_eval_run() {
1324        use wyrd_spec::ids::CardUid;
1325        let uid = |value: &str| CardUid::new(value).expect("valid uuid7");
1326        let bound = uid("01927b4c-0000-7000-8000-000000000001");
1327        let other = uid("01927b4c-0000-7000-8000-000000000002");
1328
1329        assert_eq!(
1330            "verifier:run".parse::<Permission>().expect("parses"),
1331            Permission::verifier_run_any()
1332        );
1333        assert!(Permission::verifier_run_any().covers(&Permission::verifier_run(bound.clone())));
1334        assert!(
1335            Permission::verifier_run(bound.clone())
1336                .covers(&Permission::verifier_run(bound.clone()))
1337        );
1338        assert!(
1339            !Permission::verifier_run(other).covers(&Permission::verifier_run(bound.clone())),
1340            "an unrelated UID grant does not cover"
1341        );
1342        assert!(
1343            !Permission::eval_run().covers(&Permission::verifier_run(bound.clone())),
1344            "evals:run alone does not cover"
1345        );
1346        assert_eq!(
1347            serde_json::to_value(Permission::verifier_run(bound.clone())).expect("serializes"),
1348            json!({"resource": "verifier", "action": "run", "scope": {"verifier": bound.as_str()}})
1349        );
1350        for (resource, action) in [("verifier", "read"), ("evals", "run")] {
1351            assert!(
1352                serde_json::from_value::<Permission>(json!({
1353                    "resource": resource,
1354                    "action": action,
1355                    "scope": {"verifier": bound.as_str()}
1356                }))
1357                .is_err(),
1358                "{resource}:{action}"
1359            );
1360        }
1361    }
1362
1363    /// Proves an explicit gateway grant survives merging beside `all` in either
1364    /// order and under wildcard or `AnyOf` axes, and that delegation meets
1365    /// gateway grants exactly: `all` with `all` stays `all`, explicit grants
1366    /// meet to the narrower grant, a mixed pair whose explicit result the other
1367    /// side also holds explicitly is representable, and `all` meeting an
1368    /// explicit grant alone — "that deployment, only if open" — is refused in
1369    /// both directions instead of minting the explicit grant.
1370    ///
1371    /// # Panics
1372    /// Panics when a merge drops an explicit grant, a representable meet
1373    /// differs from the exact shared authority, or an unrepresentable meet
1374    /// succeeds.
1375    #[test]
1376    fn gateway_grants_survive_merge_and_intersect_exactly() {
1377        use super::GatewayAccess;
1378        use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1379        let restricted = ProviderDeploymentName::new("restricted").expect("valid deployment");
1380        let deployment = Permission::gateway_invoke(GatewayAccess::Deployment {
1381            deployment: restricted.clone(),
1382        });
1383        let model = Permission::gateway_invoke(GatewayAccess::Model {
1384            deployment: restricted,
1385            model: ModelId::new("gpt-4o").expect("valid model"),
1386        });
1387        let any_resource_invoke = Permission {
1388            resource: Resource::AnyOf(vec![Resource::Gateway, Resource::Cards]),
1389            action: Action::Wildcard,
1390            scope: PermissionScope::All,
1391        };
1392        for broad in [
1393            Permission::gateway_invoke_any(),
1394            Permission::wildcard(),
1395            any_resource_invoke,
1396        ] {
1397            for set in [
1398                PermissionSet::from_iter([broad.clone(), deployment.clone()]),
1399                PermissionSet::from_iter([deployment.clone(), broad.clone()]),
1400            ] {
1401                assert!(set.contains_object_scoped(&model), "{broad} kept explicit");
1402                assert!(set.contains(&Permission::gateway_invoke_any()));
1403            }
1404        }
1405
1406        let all = PermissionSet::from_iter([Permission::gateway_invoke_any()]);
1407        let explicit = PermissionSet::from_iter([deployment.clone()]);
1408        let narrow = PermissionSet::from_iter([model.clone()]);
1409        assert_eq!(all.intersection(&all).expect("representable"), all);
1410        assert_eq!(
1411            explicit.intersection(&narrow).expect("representable"),
1412            narrow
1413        );
1414        assert!(all.intersection(&explicit).is_err());
1415        assert!(explicit.intersection(&all).is_err());
1416        let wildcard = PermissionSet::from_iter([Permission::wildcard()]);
1417        assert!(wildcard.intersection(&narrow).is_err());
1418
1419        let mixed = PermissionSet::from_iter([Permission::gateway_invoke_any(), deployment]);
1420        assert_eq!(mixed.intersection(&narrow).expect("representable"), narrow);
1421        assert_eq!(narrow.intersection(&mixed).expect("representable"), narrow);
1422        assert_eq!(
1423            mixed.intersection(&all).expect("representable"),
1424            all,
1425            "both hold all; only one holds the explicit grant"
1426        );
1427        assert!(
1428            PermissionSet::from_iter([Permission::card_read()])
1429                .intersection(&explicit)
1430                .expect("no overlap is the representable empty result")
1431                .is_empty()
1432        );
1433    }
1434
1435    /// Builds the schema-scoped Bifrost query-read grant the journey seeds.
1436    fn logs_schema_grant() -> Permission {
1437        Permission {
1438            resource: Resource::BifrostQuery,
1439            action: Action::Read,
1440            scope: PermissionScope::Bifrost(BifrostPermissionScope::Schema(BifrostSchemaScope {
1441                catalog: "vala".to_owned(),
1442                schema: "logs".to_owned(),
1443            })),
1444        }
1445    }
1446
1447    /// Builds the exact table-scoped Bifrost query-read requirement for one UID.
1448    fn table_requirement(schema: &str, uid: uuid::Uuid) -> Permission {
1449        Permission {
1450            resource: Resource::BifrostQuery,
1451            action: Action::Read,
1452            scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1453                catalog: "vala".to_owned(),
1454                schema: schema.to_owned(),
1455                table_uid: uid,
1456            })),
1457        }
1458    }
1459
1460    /// Proves the intersection keeps only authority both sides grant, at the
1461    /// narrower scope: wildcard, schema, exact-object, and disjoint grants.
1462    ///
1463    /// # Panics
1464    /// Panics when an intersection keeps broader, one-sided, or disjoint
1465    /// authority, or drops authority both sides grant.
1466    #[test]
1467    fn intersection_keeps_only_the_narrower_shared_authority() {
1468        let uid = uuid::Uuid::from_u128(0x77);
1469        let set = |permissions: Vec<Permission>| permissions.into_iter().collect::<PermissionSet>();
1470
1471        let wildcard = set(vec![Permission::wildcard()]);
1472        let target = set(vec![Permission::card_read(), logs_schema_grant()]);
1473        assert_eq!(
1474            wildcard.intersection(&target).expect("representable"),
1475            target
1476        );
1477        assert_eq!(
1478            target.intersection(&wildcard).expect("representable"),
1479            target
1480        );
1481
1482        let schema = set(vec![logs_schema_grant()]);
1483        let table = set(vec![table_requirement("logs", uid)]);
1484        assert_eq!(schema.intersection(&table).expect("representable"), table);
1485        assert_eq!(table.intersection(&schema).expect("representable"), table);
1486
1487        let other_table = set(vec![table_requirement("logs", uuid::Uuid::from_u128(0x78))]);
1488        assert!(
1489            table
1490                .intersection(&other_table)
1491                .expect("representable")
1492                .is_empty()
1493        );
1494        assert!(
1495            set(vec![table_requirement("traces", uid)])
1496                .intersection(&schema)
1497                .expect("representable")
1498                .is_empty()
1499        );
1500
1501        let all_reads = set(vec![Permission {
1502            resource: Resource::Wildcard,
1503            action: Action::Read,
1504            scope: PermissionScope::All,
1505        }]);
1506        let cards_any = set(vec![Permission {
1507            resource: Resource::AnyOf(vec![Resource::Cards, Resource::Audit]),
1508            action: Action::Wildcard,
1509            scope: PermissionScope::All,
1510        }]);
1511        assert_eq!(
1512            all_reads.intersection(&cards_any).expect("representable"),
1513            set(vec![Permission::card_read(), Permission::audit_read()])
1514        );
1515
1516        assert!(
1517            set(vec![Permission::card_write()])
1518                .intersection(&set(vec![
1519                    Permission::card_read(),
1520                    Permission::bifrost_record_write()
1521                ]))
1522                .expect("representable")
1523                .is_empty()
1524        );
1525    }
1526
1527    /// Proves the persisted permission JSON is exactly the approved three-field
1528    /// projection for `all`, schema, and table scope, in both directions.
1529    #[test]
1530    fn scoped_permission_json_matches_the_approved_projection() {
1531        let uid = uuid::Uuid::from_u128(0x99);
1532        let cases = [
1533            (
1534                Permission::bifrost_query_read(),
1535                json!({"resource": "bifrost_query", "action": "read", "scope": "all"}),
1536            ),
1537            (
1538                logs_schema_grant(),
1539                json!({
1540                    "resource": "bifrost_query",
1541                    "action": "read",
1542                    "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1543                }),
1544            ),
1545            (
1546                table_requirement("traces", uid),
1547                json!({
1548                    "resource": "bifrost_query",
1549                    "action": "read",
1550                    "scope": {"bifrost": {"table": {
1551                        "catalog": "vala",
1552                        "schema": "traces",
1553                        "table_uid": uid.to_string(),
1554                    }}}
1555                }),
1556            ),
1557        ];
1558
1559        for (permission, wire) in cases {
1560            assert_eq!(
1561                serde_json::to_value(&permission).expect("permission serializes"),
1562                wire
1563            );
1564            assert_eq!(
1565                serde_json::from_value::<Permission>(wire).expect("permission deserializes"),
1566                permission
1567            );
1568        }
1569    }
1570
1571    /// Proves Bifrost object scope applies to record writes as well as query
1572    /// reads, a table-scoped write covers only its own table, and the scope is
1573    /// still refused on any other resource or action pairing.
1574    #[test]
1575    fn bifrost_scope_applies_to_record_write_and_query_read_only() {
1576        let calls = uuid::Uuid::from_u128(0xca11);
1577        let spans = uuid::Uuid::from_u128(0x5a45);
1578        let table_write = |uid| Permission {
1579            resource: Resource::BifrostRecord,
1580            action: Action::Write,
1581            scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1582                catalog: "vala".to_owned(),
1583                schema: "gateway".to_owned(),
1584                table_uid: uid,
1585            })),
1586        };
1587        table_write(calls)
1588            .validate()
1589            .expect("scoped record write is valid");
1590        assert!(table_write(calls).covers(&table_write(calls)));
1591        assert!(!table_write(calls).covers(&table_write(spans)));
1592        assert!(
1593            !table_write(calls).covers(&Permission::bifrost_record_write()),
1594            "a table-scoped write never covers the unscoped requirement"
1595        );
1596        assert!(Permission::bifrost_record_write().covers(&table_write(spans)));
1597        let wire = serde_json::to_value(table_write(calls)).expect("serializes");
1598        assert_eq!(
1599            serde_json::from_value::<Permission>(wire).expect("round-trips"),
1600            table_write(calls)
1601        );
1602        for (resource, action) in [
1603            (Resource::BifrostRecord, Action::Read),
1604            (Resource::BifrostQuery, Action::Write),
1605            (Resource::BifrostTable, Action::Write),
1606        ] {
1607            let permission = Permission {
1608                resource,
1609                action,
1610                ..table_write(calls)
1611            };
1612            assert!(permission.validate().is_err(), "{permission}");
1613        }
1614    }
1615
1616    /// Proves `scope` is mandatory: the pre-scope two-field form is rejected
1617    /// rather than defaulted, so no compatibility decoder exists.
1618    #[test]
1619    fn scope_is_required_and_has_no_compatibility_decoder() {
1620        let error = serde_json::from_value::<Permission>(json!({
1621            "resource": "bifrost_query",
1622            "action": "read"
1623        }))
1624        .expect_err("the unscoped two-field form is not accepted");
1625
1626        assert!(error.to_string().contains("scope"), "{error}");
1627    }
1628
1629    /// Proves a Bifrost object scope only attaches to a resource that owns a
1630    /// Bifrost object; `cards` and `wildcard` both fail closed at decode.
1631    #[test]
1632    fn bifrost_scope_is_rejected_on_an_unrelated_resource() {
1633        for resource in ["cards", "wildcard"] {
1634            let error = serde_json::from_value::<Permission>(json!({
1635                "resource": resource,
1636                "action": "read",
1637                "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1638            }))
1639            .expect_err("a Bifrost object scope needs a Bifrost object resource");
1640
1641            assert!(error.to_string().contains("resource"), "{error}");
1642        }
1643    }
1644
1645    /// Proves Bifrost object scope is valid only for the exact `read` action.
1646    ///
1647    /// Without this, persisted or signed JSON could carry a Bifrost scope on
1648    /// `wildcard`, which then covers the required read and turns corrupt role
1649    /// JSON into effective read authority instead of a decode failure.
1650    #[test]
1651    fn bifrost_scope_is_rejected_on_a_non_read_action() {
1652        for action in [
1653            json!("write"),
1654            json!("wildcard"),
1655            json!({"any_of": ["read", "write"]}),
1656        ] {
1657            let error = serde_json::from_value::<Permission>(json!({
1658                "resource": "bifrost_query",
1659                "action": action,
1660                "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1661            }))
1662            .expect_err("a Bifrost object scope only applies to an exact read");
1663
1664            assert!(error.to_string().contains("action"), "{error}");
1665        }
1666
1667        serde_json::from_value::<Permission>(json!({
1668            "resource": "bifrost_query",
1669            "action": "read",
1670            "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1671        }))
1672        .expect("exact query read stays valid for Bifrost scope");
1673    }
1674
1675    /// Proves a structurally valid but empty object identity is not an object:
1676    /// malformed identities fail at decode, never at check time.
1677    #[test]
1678    fn malformed_scope_identity_is_rejected_at_decode() {
1679        assert!(
1680            serde_json::from_value::<Permission>(json!({
1681                "resource": "bifrost_query",
1682                "action": "read",
1683                "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": ""}}}
1684            }))
1685            .is_err(),
1686            "an empty schema segment is not an object identity"
1687        );
1688    }
1689
1690    /// Proves coverage requires resource, action, and scope together, and that
1691    /// wildcard authority stays object-wide only because its scope is `All`.
1692    #[test]
1693    fn coverage_is_three_axis() {
1694        let logs = uuid::Uuid::from_u128(1);
1695        let traces = uuid::Uuid::from_u128(2);
1696
1697        // Schema scope reaches current and future tables in that exact schema.
1698        assert!(logs_schema_grant().covers(&table_requirement("logs", logs)));
1699        assert!(logs_schema_grant().covers(&table_requirement("logs", traces)));
1700        assert!(!logs_schema_grant().covers(&table_requirement("traces", traces)));
1701
1702        // Exact table scope reaches only its own UID.
1703        let exact = table_requirement("traces", traces);
1704        assert!(exact.covers(&table_requirement("traces", traces)));
1705        assert!(!exact.covers(&table_requirement("traces", logs)));
1706
1707        // `All` reaches every object, but only for its own resource and action.
1708        assert!(Permission::bifrost_query_read().covers(&table_requirement("logs", logs)));
1709        assert!(!Permission::bifrost_table_read().covers(&table_requirement("logs", logs)));
1710
1711        // A wildcard grant is object-wide only because its scope is `All`.
1712        assert!(Permission::wildcard().covers(&table_requirement("logs", logs)));
1713
1714        // A scoped grant never satisfies an object-wide requirement.
1715        assert!(!logs_schema_grant().covers(&Permission::bifrost_query_read()));
1716    }
1717
1718    /// Proves coarse operation admission and authoritative object coverage are
1719    /// distinct: a scoped grant admits the operation but authorizes only its object.
1720    #[test]
1721    fn covers_operation_admits_a_scoped_grant_without_authorizing_an_object() {
1722        let set = PermissionSet::from_iter([logs_schema_grant()]);
1723
1724        assert!(set.covers_operation(&Resource::BifrostQuery, &Action::Read));
1725        assert!(!set.covers_operation(&Resource::BifrostTable, &Action::Read));
1726        assert!(!set.contains(&Permission::bifrost_query_read()));
1727        assert!(set.contains(&table_requirement("logs", uuid::Uuid::from_u128(7))));
1728        assert!(!set.contains(&table_requirement("traces", uuid::Uuid::from_u128(7))));
1729    }
1730
1731    /// Proves set subsumption runs on all three axes: disjoint scopes are both
1732    /// retained, and an `All`-scoped grant absorbs every narrower scope.
1733    #[test]
1734    fn permission_set_keeps_scopes_that_do_not_subsume_each_other() {
1735        let mut set = PermissionSet::from_iter([logs_schema_grant()]);
1736        set.insert(table_requirement("traces", uuid::Uuid::from_u128(3)));
1737
1738        assert_eq!(set.len(), 2);
1739
1740        set.insert(Permission::bifrost_query_read());
1741
1742        assert_eq!(set.len(), 1);
1743        assert_eq!(set.iter().next(), Some(&Permission::bifrost_query_read()));
1744    }
1745
1746    #[test]
1747    fn permission_wire_token_roundtrips() {
1748        let parsed = "cards:write"
1749            .parse::<Permission>()
1750            .expect("permission parses");
1751
1752        assert_eq!(parsed, Permission::card_write());
1753        assert_eq!(parsed.to_string(), "cards:write");
1754    }
1755}