Skip to main content

audit_trails/core/access/
mod.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Role and capability management APIs for Audit Trails.
5//!
6//! This module is the Rust-facing wrapper around the access-control state integrated into each audit trail.
7//! Roles grant [`crate::core::types::PermissionSet`] values, while capability objects bind one role to one trail and
8//! may add optional address or time restrictions.
9//!
10//! Additional record-tag constraints are represented as [`crate::core::types::RoleTags`]. They narrow which tagged
11//! records a role may operate on, but they do not replace the underlying permission checks enforced by the Move
12//! package.
13
14use iota_interaction::types::base_types::ObjectID;
15use iota_interaction::{IotaKeySignature, OptionalSync};
16use product_common::core_client::CoreClient;
17use product_common::transaction::transaction_builder::TransactionBuilder;
18use secret_storage::Signer;
19
20use crate::core::trail::AuditTrailFull;
21use crate::core::types::{CapabilityIssueOptions, PermissionSet, RoleTags};
22
23mod operations;
24mod transactions;
25
26pub use transactions::{
27    CleanupRevokedCapabilities, CreateRole, DeleteRole, DestroyCapability, DestroyInitialAdminCapability,
28    IssueCapability, RevokeCapability, RevokeInitialAdminCapability, UpdateRole,
29};
30
31/// Access-control API scoped to a specific trail.
32///
33/// This handle exposes role-management and capability-management operations for one trail. All authorization is
34/// still enforced against the capability supplied during transaction construction.
35#[derive(Debug, Clone)]
36pub struct TrailAccess<'a, C> {
37    pub(crate) client: &'a C,
38    pub(crate) trail_id: ObjectID,
39    pub(crate) selected_capability_id: Option<ObjectID>,
40}
41
42impl<'a, C> TrailAccess<'a, C> {
43    pub(crate) fn new(client: &'a C, trail_id: ObjectID, selected_capability_id: Option<ObjectID>) -> Self {
44        Self {
45            client,
46            trail_id,
47            selected_capability_id,
48        }
49    }
50
51    /// Uses the provided capability as the auth capability for subsequent write operations.
52    pub fn using_capability(mut self, capability_id: ObjectID) -> Self {
53        self.selected_capability_id = Some(capability_id);
54        self
55    }
56
57    /// Returns a role-scoped handle for the given role name.
58    ///
59    /// The returned handle only identifies the role. Existence and authorization are checked when the
60    /// resulting transaction is built and executed.
61    pub fn for_role(&self, name: impl Into<String>) -> RoleHandle<'a, C> {
62        RoleHandle::new(self.client, self.trail_id, name.into(), self.selected_capability_id)
63    }
64
65    /// Revokes an issued capability.
66    ///
67    /// Revocation adds the capability ID to the trail's denylist. Pass the capability's `valid_until` value
68    /// when it is known so later cleanup keeps the same expiry semantics.
69    pub fn revoke_capability<S>(
70        &self,
71        capability_id: ObjectID,
72        capability_valid_until: Option<u64>,
73    ) -> TransactionBuilder<RevokeCapability>
74    where
75        C: AuditTrailFull + CoreClient<S>,
76        S: Signer<IotaKeySignature> + OptionalSync,
77    {
78        let owner = self.client.sender_address();
79        TransactionBuilder::new(RevokeCapability::new(
80            self.trail_id,
81            owner,
82            capability_id,
83            capability_valid_until,
84            self.selected_capability_id,
85        ))
86    }
87
88    /// Destroys a capability object.
89    ///
90    /// This consumes the owned capability object itself. It uses the generic capability-destruction path and
91    /// therefore must not be used for initial-admin capabilities.
92    pub fn destroy_capability<S>(&self, capability_id: ObjectID) -> TransactionBuilder<DestroyCapability>
93    where
94        C: AuditTrailFull + CoreClient<S>,
95        S: Signer<IotaKeySignature> + OptionalSync,
96    {
97        let owner = self.client.sender_address();
98        TransactionBuilder::new(DestroyCapability::new(
99            self.trail_id,
100            owner,
101            capability_id,
102            self.selected_capability_id,
103        ))
104    }
105
106    /// Destroys an initial-admin capability without presenting another authorization capability.
107    ///
108    /// Initial-admin capability IDs are tracked separately, so they cannot be removed through the generic
109    /// destroy path.
110    pub fn destroy_initial_admin_capability<S>(
111        &self,
112        capability_id: ObjectID,
113    ) -> TransactionBuilder<DestroyInitialAdminCapability>
114    where
115        C: AuditTrailFull + CoreClient<S>,
116        S: Signer<IotaKeySignature> + OptionalSync,
117    {
118        TransactionBuilder::new(DestroyInitialAdminCapability::new(self.trail_id, capability_id))
119    }
120
121    /// Revokes an initial-admin capability by ID.
122    ///
123    /// Like [`TrailAccess::revoke_capability`], this writes to the denylist. The dedicated entry point exists
124    /// because initial-admin capability IDs are protected separately.
125    pub fn revoke_initial_admin_capability<S>(
126        &self,
127        capability_id: ObjectID,
128        capability_valid_until: Option<u64>,
129    ) -> TransactionBuilder<RevokeInitialAdminCapability>
130    where
131        C: AuditTrailFull + CoreClient<S>,
132        S: Signer<IotaKeySignature> + OptionalSync,
133    {
134        let owner = self.client.sender_address();
135        TransactionBuilder::new(RevokeInitialAdminCapability::new(
136            self.trail_id,
137            owner,
138            capability_id,
139            capability_valid_until,
140            self.selected_capability_id,
141        ))
142    }
143
144    /// Removes expired entries from the revoked-capability denylist.
145    ///
146    /// Only entries whose stored expiry has passed are removed. Revocations without an expiry remain until
147    /// they are explicitly destroyed or the trail is deleted.
148    pub fn cleanup_revoked_capabilities<S>(&self) -> TransactionBuilder<CleanupRevokedCapabilities>
149    where
150        C: AuditTrailFull + CoreClient<S>,
151        S: Signer<IotaKeySignature> + OptionalSync,
152    {
153        let owner = self.client.sender_address();
154        TransactionBuilder::new(CleanupRevokedCapabilities::new(
155            self.trail_id,
156            owner,
157            self.selected_capability_id,
158        ))
159    }
160}
161
162/// Role-scoped access-control API.
163///
164/// A `RoleHandle` identifies one role name inside the trail's access-control state and builds transactions that
165/// act on that role.
166#[derive(Debug, Clone)]
167pub struct RoleHandle<'a, C> {
168    pub(crate) client: &'a C,
169    pub(crate) trail_id: ObjectID,
170    pub(crate) name: String,
171    pub(crate) selected_capability_id: Option<ObjectID>,
172}
173
174impl<'a, C> RoleHandle<'a, C> {
175    pub(crate) fn new(
176        client: &'a C,
177        trail_id: ObjectID,
178        name: String,
179        selected_capability_id: Option<ObjectID>,
180    ) -> Self {
181        Self {
182            client,
183            trail_id,
184            name,
185            selected_capability_id,
186        }
187    }
188
189    /// Uses the provided capability as the auth capability for subsequent write operations.
190    pub fn using_capability(mut self, capability_id: ObjectID) -> Self {
191        self.selected_capability_id = Some(capability_id);
192        self
193    }
194
195    /// Returns the role name represented by this handle.
196    pub fn name(&self) -> &str {
197        &self.name
198    }
199
200    /// Creates this role with the provided permissions and optional role-tag
201    /// access rules.
202    ///
203    /// Any supplied [`RoleTags`] must already exist in the trail-owned tag
204    /// registry. The tag list is stored as
205    /// role data on the Move side and is later used for tag-aware record authorization.
206    pub fn create<S>(&self, permissions: PermissionSet, role_tags: Option<RoleTags>) -> TransactionBuilder<CreateRole>
207    where
208        C: AuditTrailFull + CoreClient<S>,
209        S: Signer<IotaKeySignature> + OptionalSync,
210    {
211        let owner = self.client.sender_address();
212        TransactionBuilder::new(CreateRole::new(
213            self.trail_id,
214            owner,
215            self.name.clone(),
216            permissions,
217            role_tags,
218            self.selected_capability_id,
219        ))
220    }
221
222    /// Issues a capability for this role using optional restrictions.
223    ///
224    /// The resulting capability always targets this trail and grants exactly
225    /// this role. `issued_to`,
226    /// `valid_from_ms`, and `valid_until_ms` only configure restrictions on
227    /// the issued object; enforcement
228    /// happens on-chain when the capability is later used.
229    pub fn issue_capability<S>(&self, options: CapabilityIssueOptions) -> TransactionBuilder<IssueCapability>
230    where
231        C: AuditTrailFull + CoreClient<S>,
232        S: Signer<IotaKeySignature> + OptionalSync,
233    {
234        let owner = self.client.sender_address();
235        TransactionBuilder::new(IssueCapability::new(
236            self.trail_id,
237            owner,
238            self.name.clone(),
239            options,
240            self.selected_capability_id,
241        ))
242    }
243
244    /// Updates permissions and role-tag access rules for this role.
245    ///
246    /// As with [`RoleHandle::create`], any supplied [`RoleTags`] must already
247    /// exist in the trail tag registry.
248    pub fn update_permissions<S>(
249        &self,
250        permissions: PermissionSet,
251        role_tags: Option<RoleTags>,
252    ) -> TransactionBuilder<UpdateRole>
253    where
254        C: AuditTrailFull + CoreClient<S>,
255        S: Signer<IotaKeySignature> + OptionalSync,
256    {
257        let owner = self.client.sender_address();
258        TransactionBuilder::new(UpdateRole::new(
259            self.trail_id,
260            owner,
261            self.name.clone(),
262            permissions,
263            role_tags,
264            self.selected_capability_id,
265        ))
266    }
267
268    /// Deletes this role.
269    ///
270    /// The reserved initial-admin role cannot be deleted.
271    pub fn delete<S>(&self) -> TransactionBuilder<DeleteRole>
272    where
273        C: AuditTrailFull + CoreClient<S>,
274        S: Signer<IotaKeySignature> + OptionalSync,
275    {
276        let owner = self.client.sender_address();
277        TransactionBuilder::new(DeleteRole::new(
278            self.trail_id,
279            owner,
280            self.name.clone(),
281            self.selected_capability_id,
282        ))
283    }
284}