From e44917712c0dabad9681356b54363f46b399bb25 Mon Sep 17 00:00:00 2001 From: Yasir Date: Wed, 17 Dec 2025 12:32:48 +0300 Subject: [PATCH 01/18] feat: Implement audit trails with role-based access control --- audit-trails-move/Move.lock | 42 ++ audit-trails-move/sources/audit_trails.move | 621 +++++++++++++++++++- audit-trails-move/sources/capabilities.move | 31 +- audit-trails-move/sources/permissions.move | 58 ++ notarization-move/Move.history.json | 10 +- 5 files changed, 737 insertions(+), 25 deletions(-) create mode 100644 audit-trails-move/Move.lock diff --git a/audit-trails-move/Move.lock b/audit-trails-move/Move.lock new file mode 100644 index 0000000..b38c76a --- /dev/null +++ b/audit-trails-move/Move.lock @@ -0,0 +1,42 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 3 +manifest_digest = "205525E3D4D4DF71C1144E3EE5DDD210506D20F1DB2438FC02BB2ADCE7E5BFD6" +deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C" +dependencies = [ + { id = "Iota", name = "Iota" }, + { id = "IotaSystem", name = "IotaSystem" }, + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Stardust", name = "Stardust" }, +] + +[[move.package]] +id = "Iota" +source = { git = "https://github.com/iotaledger/iota.git", rev = "4698c6723208e052a00c74602d2c8dc0efffe5de", subdir = "crates/iota-framework/packages/iota-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] + +[[move.package]] +id = "IotaSystem" +source = { git = "https://github.com/iotaledger/iota.git", rev = "4698c6723208e052a00c74602d2c8dc0efffe5de", subdir = "crates/iota-framework/packages/iota-system" } + +dependencies = [ + { id = "Iota", name = "Iota" }, + { id = "MoveStdlib", name = "MoveStdlib" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/iotaledger/iota.git", rev = "4698c6723208e052a00c74602d2c8dc0efffe5de", subdir = "crates/iota-framework/packages/move-stdlib" } + +[[move.package]] +id = "Stardust" +source = { git = "https://github.com/iotaledger/iota.git", rev = "4698c6723208e052a00c74602d2c8dc0efffe5de", subdir = "crates/iota-framework/packages/stardust" } + +dependencies = [ + { id = "Iota", name = "Iota" }, + { id = "MoveStdlib", name = "MoveStdlib" }, +] diff --git a/audit-trails-move/sources/audit_trails.move b/audit-trails-move/sources/audit_trails.move index 82e1bf1..4e5b1de 100644 --- a/audit-trails-move/sources/audit_trails.move +++ b/audit-trails-move/sources/audit_trails.move @@ -1,51 +1,636 @@ // Copyright (c) 2025 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -/// Audit Trails - Tamper-proof sequential record chains with RBAC +/// Audit trails with role-based access control and timelock +/// +/// An audit trail is a tamper-proof, sequential chain of notarized records where each entry +/// references its predecessor, ensuring verifiable continuity and integrity. +/// +/// Records are addressed by trail_id + sequence_number (Option B design). module audit_trails::audit_trails; -use iota::clock::Clock; -use iota::vec_map::VecMap; +use audit_trails::capabilities::{Self, Capability}; +use audit_trails::permissions::{Self, Permission}; +use iota::clock::{Self, Clock}; +use iota::event; +use iota::linked_table::{Self, LinkedTable}; +use iota::vec_map::{Self, VecMap}; use iota::vec_set::VecSet; use std::string::String; +// ===== Errors ===== +/// Provided previous sequence doesn't match the trail's last sequence +const EInvalidPreviousSequence: u64 = 1; +/// Capability lacks required permission or has been revoked +const EInsufficientPermissions: u64 = 2; +/// Capability is for a different trail +const EWrongCapability: u64 = 3; +/// Role doesn't exist in the trail's permission map +const ERoleNotFound: u64 = 4; +/// Attempting to create a role that already exists +const ERoleAlreadyExists: u64 = 5; +/// Role has no permissions (must have at least one) +const EEmptyRole: u64 = 6; +/// Same permission appears multiple times in input vector +const EDuplicatePermissions: u64 = 7; +/// Capability ID not in issued whitelist (forgery attempt) +const ECapabilityNotIssued: u64 = 8; +/// Signer doesn't match the capability's issued_to address +const EUnauthorizedSigner: u64 = 9; +/// Cannot remove the setup role (prevents self-bricking) +const ECannotRemoveSetupRole: u64 = 10; +/// Cannot revoke the last capability with CapAdmin permission +const ECannotRevokeLastAdmin: u64 = 11; +/// Record not found at the given sequence number +const ERecordNotFound: u64 = 12; + // ===== Core Structures ===== -/// Controls when records can be deleted +/// Controls when records can be deleted (time OR count based) public struct LockingConfig has copy, drop, store { + /// Records locked for N seconds after creation time_window_seconds: Option, + /// Last N records are always locked count_window: Option, } -/// Immutable trail metadata (set at creation) -public struct TrailMetadata has store { +/// Metadata set at trail creation (immutable) +public struct TrailImmutableMetadata has store { name: Option, description: Option, } -public struct Permission has copy, drop, store {} +/// A single record in the audit trail (stored in LinkedTable, no ObjectID) +public struct Record has store { + /// Arbitrary data stored on-chain + stored_data: D, + /// Optional metadata for this specific record + record_metadata: Option, + /// Position in the trail (0-indexed, never reused) + sequence_number: u64, + /// Who added this record + added_by: address, + /// When this record was added (milliseconds) + added_at: u64, +} -/// Shared audit trail object -public struct AuditTrail has key, store { +/// Shared audit trail object with role-based access control +/// Records are stored in a LinkedTable and addressed by sequence number +public struct AuditTrail has key, store { id: UID, + /// Address that created this trail + creator: address, + /// Creation timestamp (milliseconds) + created_at: u64, + /// Total records ever added (also serves as next sequence number) + record_count: u64, + /// Records stored by sequence number (0-indexed) + records: LinkedTable>, + /// Deletion locking rules locking_config: LockingConfig, + /// Role name → set of permissions permissions: VecMap>, - immutable_metadata: TrailMetadata, + /// Set at creation, cannot be changed + immutable_metadata: TrailImmutableMetadata, + /// Can be updated by holders of MetadataUpdate permission updatable_metadata: Option, + /// Whitelist of all issued capability IDs issued_capabilities: VecSet, +} + +// ===== Events ===== + +/// Emitted when a new trail is created +public struct AuditTrailCreated has copy, drop { + trail_id: ID, creator: address, - created_at: u64, - record_count: u64, + timestamp: u64, + has_initial_record: bool, } -/// A single record in the audit trail -public struct Record has key, store { - id: UID, +/// Emitted when a record is added to the trail +/// Records are identified by trail_id + sequence_number +public struct RecordAdded has copy, drop { trail_id: ID, + sequence_number: u64, + added_by: address, + timestamp: u64, +} + +/// Emitted when a new role is defined +public struct RoleCreated has copy, drop { + trail_id: ID, + role: String, + created_by: address, +} + +/// Emitted when role permissions are modified +public struct RoleUpdated has copy, drop { + trail_id: ID, + role: String, + updated_by: address, +} + +/// Emitted when a role is deleted +public struct RoleRemoved has copy, drop { + trail_id: ID, + role: String, + removed_by: address, +} + +/// Emitted when a capability is revoked (removed from whitelist) +public struct CapabilityRevoked has copy, drop { + trail_id: ID, + capability_id: ID, + revoked_by: address, + timestamp: u64, +} + +/// Emitted when a revoked capability is reinstated (re-added to whitelist) +public struct CapabilityReinstated has copy, drop { + trail_id: ID, + capability_id: ID, + reinstated_by: address, + timestamp: u64, +} + +/// Emitted when a trail is deleted +public struct AuditTrailDeleted has copy, drop {} + +// ===== Constructors ===== + +public fun new_locking_config( + time_window_seconds: Option, + count_window: Option, +): LockingConfig { + LockingConfig { time_window_seconds, count_window } +} + +public fun new_trail_metadata( + name: Option, + description: Option, +): TrailImmutableMetadata { + TrailImmutableMetadata { name, description } +} + +// ===== Trail Creation ===== + +/// Create a new audit trail with optional initial record +public fun create_audit_trail( + initial_data: Option, + initial_record_metadata: Option, + locking_config: LockingConfig, + trail_metadata: TrailImmutableMetadata, + updatable_metadata: Option, + clock: &Clock, + ctx: &mut TxContext, +): ID { + let creator = tx_context::sender(ctx); + let timestamp = clock::timestamp_ms(clock); + + let trail_id = object::new(ctx); + let trail_id_inner = object::uid_to_inner(&trail_id); + + let mut records = linked_table::new>(ctx); + let mut record_count = 0; + let has_initial_record = initial_data.is_some(); + + if (initial_data.is_some()) { + let record = Record { + stored_data: initial_data.destroy_some(), + record_metadata: initial_record_metadata, + sequence_number: 0, + added_by: creator, + added_at: timestamp, + }; + + linked_table::push_back(&mut records, 0, record); + record_count = 1; + + event::emit(RecordAdded { + trail_id: trail_id_inner, + sequence_number: 0, + added_by: creator, + timestamp, + }); + } else { + initial_data.destroy_none(); + }; + + // TODO: Initialize setup role with admin permissions (bootstrap) + // The creator should receive a setup capability with PermissionAdmin + CapAdmin + // to configure roles and issue capabilities to other users. + // + + let trail = AuditTrail { + id: trail_id, + creator, + created_at: timestamp, + record_count, + records, + locking_config, + permissions: vec_map::empty(), + immutable_metadata: trail_metadata, + updatable_metadata, + issued_capabilities: iota::vec_set::empty(), + }; + + transfer::share_object(trail); + + event::emit(AuditTrailCreated { + trail_id: trail_id_inner, + creator, + timestamp, + has_initial_record, + }); + + trail_id_inner +} + +// ===== Record Operations ===== + +/// Add a record to the trail +/// +/// Validates capability permissions before allowing the operation. +/// Records are added sequentially. Use expected_sequence for optimistic concurrency. +public fun add_record( + trail: &mut AuditTrail, + cap: &Capability, stored_data: D, record_metadata: Option, - previous_record_id: Option, + expected_sequence: Option, + clock: &Clock, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::record_add(), ctx); + + let caller = tx_context::sender(ctx); + let timestamp = clock::timestamp_ms(clock); + let trail_id = object::uid_to_inner(&trail.id); + + // Validate expected sequence for optimistic concurrency + if (expected_sequence.is_some()) { + let expected = *expected_sequence.borrow(); + assert!(expected == trail.record_count, EInvalidPreviousSequence); + }; + + let sequence_number = trail.record_count; + + let record = Record { + stored_data, + record_metadata, + sequence_number, + added_by: caller, + added_at: timestamp, + }; + + linked_table::push_back(&mut trail.records, sequence_number, record); + trail.record_count = trail.record_count + 1; + + event::emit(RecordAdded { + trail_id, + sequence_number, + added_by: caller, + timestamp, + }); +} + +// ===== Role & Permission Management ===== + +/// Create a new role with specific permissions +public fun create_role( + trail: &mut AuditTrail, + cap: &Capability, + role_name: String, + permissions_vec: vector, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::permission_admin(), ctx); + assert!(!trail.permissions.contains(&role_name), ERoleAlreadyExists); + + validate_permissions(&permissions_vec); + + let perms = permissions::from_vec(permissions_vec); + vec_map::insert(&mut trail.permissions, role_name, perms); + + event::emit(RoleCreated { + trail_id: object::uid_to_inner(&trail.id), + role: role_name, + created_by: tx_context::sender(ctx), + }); +} + +/// Update permissions for an existing role +public fun update_role( + trail: &mut AuditTrail, + cap: &Capability, + role_name: String, + permissions_vec: vector, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::permission_admin(), ctx); + assert!(trail.permissions.contains(&role_name), ERoleNotFound); + + validate_permissions(&permissions_vec); + + // TODO: Implement role update logic + + event::emit(RoleUpdated { + trail_id: object::uid_to_inner(&trail.id), + role: role_name, + updated_by: tx_context::sender(ctx), + }); +} + +/// Remove a role from the trail +public fun remove_role( + trail: &mut AuditTrail, + cap: &Capability, + role_name: String, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::permission_admin(), ctx); + assert!(trail.permissions.contains(&role_name), ERoleNotFound); + + // Protect setup role from removal to prevent self-bricking + assert!(role_name != std::string::utf8(b"setup"), ECannotRemoveSetupRole); + + // TODO: Implement role removal logic + // vec_map::remove(&mut trail.permissions, &role_name); + + event::emit(RoleRemoved { + trail_id: object::uid_to_inner(&trail.id), + role: role_name, + removed_by: tx_context::sender(ctx), + }); +} + +/// Validate permissions vector for duplicates and emptiness +fun validate_permissions(permissions_vec: &vector) { + assert!(!permissions_vec.is_empty(), EEmptyRole); + + let len = permissions_vec.length(); + let mut i = 0; + while (i < len) { + let perm = &permissions_vec[i]; + let mut j = i + 1; + while (j < len) { + assert!(perm != &permissions_vec[j], EDuplicatePermissions); + j = j + 1; + }; + i = i + 1; + }; +} + +/// Internal permission check (validates cap, role, permission, and signer) +fun check_permission( + trail: &AuditTrail, + cap: &Capability, + _required: &Permission, + _ctx: &TxContext, +) { + let cap_id = capabilities::cap_id(cap); + + // Verify capability is in whitelist (not issued = revoked or forged) + assert!(trail.issued_capabilities.contains(&cap_id), ECapabilityNotIssued); + + // TODO: Implement full permission checks once capabilities have role info + // 1. Verify capability is for this trail + // 2. Verify signer matches the capability holder + // 3. Verify role exists + // 4. Verify role has the required permission +} + +/// Check if a role has CapAdmin permission +fun role_has_cap_admin(trail: &AuditTrail, role: &String): bool { + if (!trail.permissions.contains(role)) { + return false + }; + let role_perms = vec_map::get(&trail.permissions, role); + permissions::has_permission(role_perms, &permissions::cap_admin()) +} + +// ===== Capability Management ===== + +/// Issue a new capability with a specific role +public fun issue_capability( + trail: &mut AuditTrail, + cap: &Capability, + role: String, + recipient: address, + _clock: &Clock, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::cap_admin(), ctx); + assert!(trail.permissions.contains(&role), ERoleNotFound); + // TODO: Implement capability issuance logic +} + +/// Revoke a capability by ID +public fun revoke_capability( + trail: &mut AuditTrail, + cap: &Capability, + cap_id: ID, + clock: &Clock, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::cap_admin(), ctx); + // TODO: Implement capability revocation logic +} + +/// Revoke multiple capabilities +public fun revoke_capabilities( + trail: &mut AuditTrail, + cap: &Capability, + cap_ids: vector, + clock: &Clock, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::cap_admin(), ctx); + + // TODO: Implement capability revocation logic +} + +/// Reinstate a previously revoked capability +public fun reinstate_capability( + trail: &mut AuditTrail, + cap: &Capability, + cap_id: ID, + clock: &Clock, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::cap_admin(), ctx); + + // TODO: Implement capability reinstatement logic +} + +/// Check if a capability has been revoked +public fun is_capability_revoked(trail: &AuditTrail, cap_id: ID): bool { + !trail.issued_capabilities.contains(&cap_id) +} + +// ===== Metadata & Locking Management ===== + +public fun update_metadata( + trail: &mut AuditTrail, + cap: &Capability, + new_metadata: Option, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::metadata_update(), ctx); + trail.updatable_metadata = new_metadata; +} + +public fun update_locking_config( + trail: &mut AuditTrail, + cap: &Capability, + new_config: LockingConfig, + ctx: &mut TxContext, +) { + check_permission(trail, cap, &permissions::locking_update(), ctx); + trail.locking_config = new_config; +} + +/// Check if a record is locked (cannot be deleted) +public fun is_record_locked( + trail: &AuditTrail, sequence_number: u64, - added_by: address, - added_at: u64, + clock: &Clock, +): bool { + assert!(linked_table::contains(&trail.records, sequence_number), ERecordNotFound); + + let record = linked_table::borrow(&trail.records, sequence_number); + let current_time = clock::timestamp_ms(clock); + + if (trail.locking_config.time_window_seconds.is_some()) { + let time_window_ms = (*trail.locking_config.time_window_seconds.borrow() as u64) * 1000; + let record_age = current_time - record.added_at; + if (record_age < time_window_ms) return true + }; + + if (trail.locking_config.count_window.is_some()) { + let count_window = *trail.locking_config.count_window.borrow(); + let records_after = trail.record_count - sequence_number - 1; + if (records_after < count_window) return true + }; + + false +} + +public fun destroy_and_revoke_capability( + trail: &mut AuditTrail, + cap: Capability, +) { + let cap_id = capabilities::cap_id(&cap); + trail.issued_capabilities.remove(&cap_id); + capabilities::destroy_capability(cap); +} + +// ===== Query Functions ===== + +/// Get the total number of records in the trail +public fun record_count(trail: &AuditTrail): u64 { + trail.record_count +} + +/// Get the trail creator address +public fun creator(trail: &AuditTrail): address { + trail.creator +} + +/// Get the trail creation timestamp +public fun created_at(trail: &AuditTrail): u64 { + trail.created_at +} + +/// Get the trail's object ID +public fun trail_id(trail: &AuditTrail): ID { + object::uid_to_inner(&trail.id) +} + +/// Get the trail name (immutable metadata) +public fun trail_name(trail: &AuditTrail): &Option { + &trail.immutable_metadata.name +} + +/// Get the trail description (immutable metadata) +public fun trail_description(trail: &AuditTrail): &Option { + &trail.immutable_metadata.description +} + +/// Get the updatable metadata +public fun updatable_metadata(trail: &AuditTrail): &Option { + &trail.updatable_metadata +} + +/// Get the locking configuration +public fun locking_config(trail: &AuditTrail): &LockingConfig { + &trail.locking_config +} + +/// Get permissions for a specific role +public fun role_permissions( + trail: &AuditTrail, + role: &String, +): &VecSet { + vec_map::get(&trail.permissions, role) +} + +/// Check if a role exists +public fun has_role(trail: &AuditTrail, role: &String): bool { + trail.permissions.contains(role) +} + +/// Check if the trail is empty (no records) +public fun is_empty(trail: &AuditTrail): bool { + linked_table::is_empty(&trail.records) +} + +/// Get the first sequence number (0 if trail has records) +public fun first_sequence(trail: &AuditTrail): Option { + *linked_table::front(&trail.records) +} + +/// Get the last sequence number +public fun last_sequence(trail: &AuditTrail): Option { + *linked_table::back(&trail.records) +} + +// ===== Record Query Functions ===== + +/// Get a record by sequence number +public fun get_record(trail: &AuditTrail, sequence_number: u64): &Record { + assert!(linked_table::contains(&trail.records, sequence_number), ERecordNotFound); + + linked_table::borrow(&trail.records, sequence_number) +} + +/// Check if a record exists at the given sequence number +public fun has_record(trail: &AuditTrail, sequence_number: u64): bool { + linked_table::contains(&trail.records, sequence_number) +} + +/// Get the stored data from a record +public fun record_data(record: &Record): &D { + &record.stored_data +} + +/// Get the record metadata +public fun record_metadata(record: &Record): &Option { + &record.record_metadata +} + +/// Get the record sequence number +public fun record_sequence_number(record: &Record): u64 { + record.sequence_number +} + +/// Get who added the record +public fun record_added_by(record: &Record): address { + record.added_by +} + +/// Get when the record was added +public fun record_added_at(record: &Record): u64 { + record.added_at } diff --git a/audit-trails-move/sources/capabilities.move b/audit-trails-move/sources/capabilities.move index 23172c2..fe49bc5 100644 --- a/audit-trails-move/sources/capabilities.move +++ b/audit-trails-move/sources/capabilities.move @@ -4,5 +4,32 @@ /// Role-based access control capabilities for audit trails module audit_trails::capabilities; -use iota::clock::Clock; -use std::string::String; +/// Capability granting role-based access to an audit trail +public struct Capability has key, store { + id: UID, +} + +/// Create a setup capability for trail initialization +public fun new_setup_cap(ctx: &mut TxContext): Capability { + Capability { + id: object::new(ctx), + } +} + +/// Create a new capability with a specific role +public fun new_capability(ctx: &mut TxContext): Capability { + Capability { + id: object::new(ctx), + } +} + +/// Get the capability's ID +public fun cap_id(cap: &Capability): ID { + object::uid_to_inner(&cap.id) +} + +/// Destroy a capability +public fun destroy_capability(cap: Capability) { + let Capability { id } = cap; + object::delete(id); +} diff --git a/audit-trails-move/sources/permissions.move b/audit-trails-move/sources/permissions.move index 4da4229..f76d714 100644 --- a/audit-trails-move/sources/permissions.move +++ b/audit-trails-move/sources/permissions.move @@ -3,3 +3,61 @@ /// Permission system for role-based access control module audit_trails::permissions; + +use iota::vec_set::{Self, VecSet}; + +public struct Permission has copy, drop, store { + value: u8, +} + +/// Create an empty permission set +public fun empty(): VecSet { + vec_set::empty() +} + +/// Add a permission to a set +public fun add(set: &mut VecSet, perm: Permission) { + vec_set::insert(set, perm); +} + +/// Create a permission set from a vector +public fun from_vec(perms: vector): VecSet { + let mut set = vec_set::empty(); + let mut i = 0; + let len = perms.length(); + while (i < len) { + vec_set::insert(&mut set, perms[i]); + i = i + 1; + }; + set +} + +/// Check if a set contains a specific permission +public fun has_permission(set: &VecSet, perm: &Permission): bool { + vec_set::contains(set, perm) +} + +/// Permission to manage roles and permissions +public fun permission_admin(): Permission { + Permission { value: 0 } +} + +/// Permission to issue/revoke capabilities +public fun cap_admin(): Permission { + Permission { value: 0 } +} + +/// Permission to add records to the trail +public fun record_add(): Permission { + Permission { value: 0 } +} + +/// Permission to update trail metadata +public fun metadata_update(): Permission { + Permission { value: 0 } +} + +/// Permission to update locking configuration +public fun locking_update(): Permission { + Permission { value: 0 } +} diff --git a/notarization-move/Move.history.json b/notarization-move/Move.history.json index fa0db1b..8f6fee0 100644 --- a/notarization-move/Move.history.json +++ b/notarization-move/Move.history.json @@ -1,18 +1,18 @@ { "aliases": { - "mainnet": "6364aad5", + "testnet": "2304aa97", "devnet": "e678123a", - "testnet": "2304aa97" + "mainnet": "6364aad5" }, "envs": { "e678123a": [ "0x0d88bcecde97585d50207a029a85d7ea0bacf73ab741cbaa975a6e279251033a" ], - "2304aa97": [ - "0x00412bd469b7f980227c6c574090348239852e43aa07818b315854fdd8a2d25f" - ], "6364aad5": [ "0x909ce9dcd9a5e97b7b8884fac8e018fad9dece348bf73837379b8694ff684cf3" + ], + "2304aa97": [ + "0x00412bd469b7f980227c6c574090348239852e43aa07818b315854fdd8a2d25f" ] } } \ No newline at end of file From c2b11bd63782814791a45b9a36f24a02c22ba3e8 Mon Sep 17 00:00:00 2001 From: Yasir Date: Wed, 17 Dec 2025 14:44:38 +0300 Subject: [PATCH 02/18] feat: Introduce locking and record modules for audit trails --- audit-trails-move/sources/audit_trails.move | 452 +++----------------- audit-trails-move/sources/locking.move | 103 +++++ audit-trails-move/sources/record.move | 70 +++ 3 files changed, 241 insertions(+), 384 deletions(-) create mode 100644 audit-trails-move/sources/locking.move create mode 100644 audit-trails-move/sources/record.move diff --git a/audit-trails-move/sources/audit_trails.move b/audit-trails-move/sources/audit_trails.move index 4e5b1de..89aebd3 100644 --- a/audit-trails-move/sources/audit_trails.move +++ b/audit-trails-move/sources/audit_trails.move @@ -6,11 +6,13 @@ /// An audit trail is a tamper-proof, sequential chain of notarized records where each entry /// references its predecessor, ensuring verifiable continuity and integrity. /// -/// Records are addressed by trail_id + sequence_number (Option B design). +/// Records are addressed by trail_id + sequence_number module audit_trails::audit_trails; use audit_trails::capabilities::{Self, Capability}; +use audit_trails::locking::{Self, LockingConfig}; use audit_trails::permissions::{Self, Permission}; +use audit_trails::record::{Self, Record}; use iota::clock::{Self, Clock}; use iota::event; use iota::linked_table::{Self, LinkedTable}; @@ -19,61 +21,17 @@ use iota::vec_set::VecSet; use std::string::String; // ===== Errors ===== -/// Provided previous sequence doesn't match the trail's last sequence -const EInvalidPreviousSequence: u64 = 1; -/// Capability lacks required permission or has been revoked -const EInsufficientPermissions: u64 = 2; -/// Capability is for a different trail -const EWrongCapability: u64 = 3; -/// Role doesn't exist in the trail's permission map -const ERoleNotFound: u64 = 4; -/// Attempting to create a role that already exists -const ERoleAlreadyExists: u64 = 5; -/// Role has no permissions (must have at least one) -const EEmptyRole: u64 = 6; -/// Same permission appears multiple times in input vector -const EDuplicatePermissions: u64 = 7; -/// Capability ID not in issued whitelist (forgery attempt) -const ECapabilityNotIssued: u64 = 8; -/// Signer doesn't match the capability's issued_to address -const EUnauthorizedSigner: u64 = 9; -/// Cannot remove the setup role (prevents self-bricking) -const ECannotRemoveSetupRole: u64 = 10; -/// Cannot revoke the last capability with CapAdmin permission -const ECannotRevokeLastAdmin: u64 = 11; -/// Record not found at the given sequence number -const ERecordNotFound: u64 = 12; +#[error] +const ERecordNotFound: vector = b"Record not found at the given sequence number"; // ===== Core Structures ===== -/// Controls when records can be deleted (time OR count based) -public struct LockingConfig has copy, drop, store { - /// Records locked for N seconds after creation - time_window_seconds: Option, - /// Last N records are always locked - count_window: Option, -} - /// Metadata set at trail creation (immutable) -public struct TrailImmutableMetadata has store { +public struct TrailImmutableMetadata has copy, drop, store { name: Option, description: Option, } -/// A single record in the audit trail (stored in LinkedTable, no ObjectID) -public struct Record has store { - /// Arbitrary data stored on-chain - stored_data: D, - /// Optional metadata for this specific record - record_metadata: Option, - /// Position in the trail (0-indexed, never reused) - sequence_number: u64, - /// Who added this record - added_by: address, - /// When this record was added (milliseconds) - added_at: u64, -} - /// Shared audit trail object with role-based access control /// Records are stored in a LinkedTable and addressed by sequence number public struct AuditTrail has key, store { @@ -88,13 +46,13 @@ public struct AuditTrail has key, store { records: LinkedTable>, /// Deletion locking rules locking_config: LockingConfig, - /// Role name → set of permissions + /// Role name -> set of permissions (TODO: implement) permissions: VecMap>, /// Set at creation, cannot be changed immutable_metadata: TrailImmutableMetadata, /// Can be updated by holders of MetadataUpdate permission updatable_metadata: Option, - /// Whitelist of all issued capability IDs + /// Whitelist of all issued capability IDs (TODO: implement) issued_capabilities: VecSet, } @@ -117,55 +75,9 @@ public struct RecordAdded has copy, drop { timestamp: u64, } -/// Emitted when a new role is defined -public struct RoleCreated has copy, drop { - trail_id: ID, - role: String, - created_by: address, -} - -/// Emitted when role permissions are modified -public struct RoleUpdated has copy, drop { - trail_id: ID, - role: String, - updated_by: address, -} - -/// Emitted when a role is deleted -public struct RoleRemoved has copy, drop { - trail_id: ID, - role: String, - removed_by: address, -} - -/// Emitted when a capability is revoked (removed from whitelist) -public struct CapabilityRevoked has copy, drop { - trail_id: ID, - capability_id: ID, - revoked_by: address, - timestamp: u64, -} - -/// Emitted when a revoked capability is reinstated (re-added to whitelist) -public struct CapabilityReinstated has copy, drop { - trail_id: ID, - capability_id: ID, - reinstated_by: address, - timestamp: u64, -} - -/// Emitted when a trail is deleted -public struct AuditTrailDeleted has copy, drop {} - // ===== Constructors ===== -public fun new_locking_config( - time_window_seconds: Option, - count_window: Option, -): LockingConfig { - LockingConfig { time_window_seconds, count_window } -} - +/// Create immutable trail metadata public fun new_trail_metadata( name: Option, description: Option, @@ -176,7 +88,7 @@ public fun new_trail_metadata( // ===== Trail Creation ===== /// Create a new audit trail with optional initial record -public fun create_audit_trail( +public fun create( initial_data: Option, initial_record_metadata: Option, locking_config: LockingConfig, @@ -185,30 +97,30 @@ public fun create_audit_trail( clock: &Clock, ctx: &mut TxContext, ): ID { - let creator = tx_context::sender(ctx); + let creator = ctx.sender(); let timestamp = clock::timestamp_ms(clock); - let trail_id = object::new(ctx); - let trail_id_inner = object::uid_to_inner(&trail_id); + let trail_uid = object::new(ctx); + let trail_id = object::uid_to_inner(&trail_uid); let mut records = linked_table::new>(ctx); let mut record_count = 0; let has_initial_record = initial_data.is_some(); if (initial_data.is_some()) { - let record = Record { - stored_data: initial_data.destroy_some(), - record_metadata: initial_record_metadata, - sequence_number: 0, - added_by: creator, - added_at: timestamp, - }; + let record = record::new( + initial_data.destroy_some(), + initial_record_metadata, + 0, // sequence_number + creator, + timestamp, + ); linked_table::push_back(&mut records, 0, record); record_count = 1; event::emit(RecordAdded { - trail_id: trail_id_inner, + trail_id, sequence_number: 0, added_by: creator, timestamp, @@ -220,10 +132,9 @@ public fun create_audit_trail( // TODO: Initialize setup role with admin permissions (bootstrap) // The creator should receive a setup capability with PermissionAdmin + CapAdmin // to configure roles and issue capabilities to other users. - // let trail = AuditTrail { - id: trail_id, + id: trail_uid, creator, created_at: timestamp, record_count, @@ -238,51 +149,44 @@ public fun create_audit_trail( transfer::share_object(trail); event::emit(AuditTrailCreated { - trail_id: trail_id_inner, + trail_id, creator, timestamp, has_initial_record, }); - trail_id_inner + trail_id } // ===== Record Operations ===== /// Add a record to the trail /// -/// Validates capability permissions before allowing the operation. -/// Records are added sequentially. Use expected_sequence for optimistic concurrency. +/// Records are added sequentially with auto-assigned sequence numbers. +/// +/// TODO: Add capability parameter and permission check once implemented public fun add_record( trail: &mut AuditTrail, cap: &Capability, stored_data: D, record_metadata: Option, - expected_sequence: Option, clock: &Clock, ctx: &mut TxContext, ) { - check_permission(trail, cap, &permissions::record_add(), ctx); + // TODO: check_permission(trail, cap, &permissions::record_add(), ctx); - let caller = tx_context::sender(ctx); + let caller = ctx.sender(); let timestamp = clock::timestamp_ms(clock); let trail_id = object::uid_to_inner(&trail.id); - - // Validate expected sequence for optimistic concurrency - if (expected_sequence.is_some()) { - let expected = *expected_sequence.borrow(); - assert!(expected == trail.record_count, EInvalidPreviousSequence); - }; - let sequence_number = trail.record_count; - let record = Record { + let record = record::new( stored_data, record_metadata, sequence_number, - added_by: caller, - added_at: timestamp, - }; + caller, + timestamp, + ); linked_table::push_back(&mut trail.records, sequence_number, record); trail.record_count = trail.record_count + 1; @@ -295,238 +199,57 @@ public fun add_record( }); } -// ===== Role & Permission Management ===== - -/// Create a new role with specific permissions -public fun create_role( - trail: &mut AuditTrail, - cap: &Capability, - role_name: String, - permissions_vec: vector, - ctx: &mut TxContext, -) { - check_permission(trail, cap, &permissions::permission_admin(), ctx); - assert!(!trail.permissions.contains(&role_name), ERoleAlreadyExists); - - validate_permissions(&permissions_vec); - - let perms = permissions::from_vec(permissions_vec); - vec_map::insert(&mut trail.permissions, role_name, perms); - - event::emit(RoleCreated { - trail_id: object::uid_to_inner(&trail.id), - role: role_name, - created_by: tx_context::sender(ctx), - }); -} - -/// Update permissions for an existing role -public fun update_role( - trail: &mut AuditTrail, - cap: &Capability, - role_name: String, - permissions_vec: vector, - ctx: &mut TxContext, -) { - check_permission(trail, cap, &permissions::permission_admin(), ctx); - assert!(trail.permissions.contains(&role_name), ERoleNotFound); - - validate_permissions(&permissions_vec); - - // TODO: Implement role update logic - - event::emit(RoleUpdated { - trail_id: object::uid_to_inner(&trail.id), - role: role_name, - updated_by: tx_context::sender(ctx), - }); -} +// ===== Locking ===== -/// Remove a role from the trail -public fun remove_role( - trail: &mut AuditTrail, - cap: &Capability, - role_name: String, - ctx: &mut TxContext, -) { - check_permission(trail, cap, &permissions::permission_admin(), ctx); - assert!(trail.permissions.contains(&role_name), ERoleNotFound); - - // Protect setup role from removal to prevent self-bricking - assert!(role_name != std::string::utf8(b"setup"), ECannotRemoveSetupRole); - - // TODO: Implement role removal logic - // vec_map::remove(&mut trail.permissions, &role_name); - - event::emit(RoleRemoved { - trail_id: object::uid_to_inner(&trail.id), - role: role_name, - removed_by: tx_context::sender(ctx), - }); -} - -/// Validate permissions vector for duplicates and emptiness -fun validate_permissions(permissions_vec: &vector) { - assert!(!permissions_vec.is_empty(), EEmptyRole); - - let len = permissions_vec.length(); - let mut i = 0; - while (i < len) { - let perm = &permissions_vec[i]; - let mut j = i + 1; - while (j < len) { - assert!(perm != &permissions_vec[j], EDuplicatePermissions); - j = j + 1; - }; - i = i + 1; - }; -} - -/// Internal permission check (validates cap, role, permission, and signer) -fun check_permission( +/// Check if a record is locked (cannot be deleted) +public fun is_record_locked( trail: &AuditTrail, - cap: &Capability, - _required: &Permission, - _ctx: &TxContext, -) { - let cap_id = capabilities::cap_id(cap); - - // Verify capability is in whitelist (not issued = revoked or forged) - assert!(trail.issued_capabilities.contains(&cap_id), ECapabilityNotIssued); - - // TODO: Implement full permission checks once capabilities have role info - // 1. Verify capability is for this trail - // 2. Verify signer matches the capability holder - // 3. Verify role exists - // 4. Verify role has the required permission -} - -/// Check if a role has CapAdmin permission -fun role_has_cap_admin(trail: &AuditTrail, role: &String): bool { - if (!trail.permissions.contains(role)) { - return false - }; - let role_perms = vec_map::get(&trail.permissions, role); - permissions::has_permission(role_perms, &permissions::cap_admin()) -} - -// ===== Capability Management ===== - -/// Issue a new capability with a specific role -public fun issue_capability( - trail: &mut AuditTrail, - cap: &Capability, - role: String, - recipient: address, - _clock: &Clock, - ctx: &mut TxContext, -) { - check_permission(trail, cap, &permissions::cap_admin(), ctx); - assert!(trail.permissions.contains(&role), ERoleNotFound); - // TODO: Implement capability issuance logic -} - -/// Revoke a capability by ID -public fun revoke_capability( - trail: &mut AuditTrail, - cap: &Capability, - cap_id: ID, + sequence_number: u64, clock: &Clock, - ctx: &mut TxContext, -) { - check_permission(trail, cap, &permissions::cap_admin(), ctx); - // TODO: Implement capability revocation logic -} +): bool { + assert!(linked_table::contains(&trail.records, sequence_number), ERecordNotFound); -/// Revoke multiple capabilities -public fun revoke_capabilities( - trail: &mut AuditTrail, - cap: &Capability, - cap_ids: vector, - clock: &Clock, - ctx: &mut TxContext, -) { - check_permission(trail, cap, &permissions::cap_admin(), ctx); + let record = linked_table::borrow(&trail.records, sequence_number); + let current_time = clock::timestamp_ms(clock); - // TODO: Implement capability revocation logic + locking::is_locked( + &trail.locking_config, + sequence_number, + record::added_at(record), + trail.record_count, + current_time, + ) } -/// Reinstate a previously revoked capability -public fun reinstate_capability( +/// Update the locking configuration +/// +/// TODO: Add capability parameter and permission check once implemented +public fun update_locking_config( trail: &mut AuditTrail, cap: &Capability, - cap_id: ID, - clock: &Clock, - ctx: &mut TxContext, + new_config: LockingConfig, + _ctx: &mut TxContext, ) { - check_permission(trail, cap, &permissions::cap_admin(), ctx); - - // TODO: Implement capability reinstatement logic -} - -/// Check if a capability has been revoked -public fun is_capability_revoked(trail: &AuditTrail, cap_id: ID): bool { - !trail.issued_capabilities.contains(&cap_id) + // TODO: check_permission(trail, cap, &permissions::locking_update(), ctx); + trail.locking_config = new_config; } -// ===== Metadata & Locking Management ===== +// ===== Metadata ===== +/// Update the trail's mutable metadata +/// +/// TODO: Add capability parameter and permission check once implemented public fun update_metadata( trail: &mut AuditTrail, cap: &Capability, new_metadata: Option, - ctx: &mut TxContext, + _ctx: &mut TxContext, ) { - check_permission(trail, cap, &permissions::metadata_update(), ctx); + // TODO: check_permission(trail, cap, &permissions::metadata_update(), ctx); trail.updatable_metadata = new_metadata; } -public fun update_locking_config( - trail: &mut AuditTrail, - cap: &Capability, - new_config: LockingConfig, - ctx: &mut TxContext, -) { - check_permission(trail, cap, &permissions::locking_update(), ctx); - trail.locking_config = new_config; -} - -/// Check if a record is locked (cannot be deleted) -public fun is_record_locked( - trail: &AuditTrail, - sequence_number: u64, - clock: &Clock, -): bool { - assert!(linked_table::contains(&trail.records, sequence_number), ERecordNotFound); - - let record = linked_table::borrow(&trail.records, sequence_number); - let current_time = clock::timestamp_ms(clock); - - if (trail.locking_config.time_window_seconds.is_some()) { - let time_window_ms = (*trail.locking_config.time_window_seconds.borrow() as u64) * 1000; - let record_age = current_time - record.added_at; - if (record_age < time_window_ms) return true - }; - - if (trail.locking_config.count_window.is_some()) { - let count_window = *trail.locking_config.count_window.borrow(); - let records_after = trail.record_count - sequence_number - 1; - if (records_after < count_window) return true - }; - - false -} - -public fun destroy_and_revoke_capability( - trail: &mut AuditTrail, - cap: Capability, -) { - let cap_id = capabilities::cap_id(&cap); - trail.issued_capabilities.remove(&cap_id); - capabilities::destroy_capability(cap); -} - -// ===== Query Functions ===== +// ===== Trail Query Functions ===== /// Get the total number of records in the trail public fun record_count(trail: &AuditTrail): u64 { @@ -549,17 +272,17 @@ public fun trail_id(trail: &AuditTrail): ID { } /// Get the trail name (immutable metadata) -public fun trail_name(trail: &AuditTrail): &Option { +public fun name(trail: &AuditTrail): &Option { &trail.immutable_metadata.name } /// Get the trail description (immutable metadata) -public fun trail_description(trail: &AuditTrail): &Option { +public fun description(trail: &AuditTrail): &Option { &trail.immutable_metadata.description } /// Get the updatable metadata -public fun updatable_metadata(trail: &AuditTrail): &Option { +public fun metadata(trail: &AuditTrail): &Option { &trail.updatable_metadata } @@ -568,30 +291,17 @@ public fun locking_config(trail: &AuditTrail): &LockingConfi &trail.locking_config } -/// Get permissions for a specific role -public fun role_permissions( - trail: &AuditTrail, - role: &String, -): &VecSet { - vec_map::get(&trail.permissions, role) -} - -/// Check if a role exists -public fun has_role(trail: &AuditTrail, role: &String): bool { - trail.permissions.contains(role) -} - /// Check if the trail is empty (no records) public fun is_empty(trail: &AuditTrail): bool { linked_table::is_empty(&trail.records) } -/// Get the first sequence number (0 if trail has records) +/// Get the first sequence number (None if empty) public fun first_sequence(trail: &AuditTrail): Option { *linked_table::front(&trail.records) } -/// Get the last sequence number +/// Get the last sequence number (None if empty) public fun last_sequence(trail: &AuditTrail): Option { *linked_table::back(&trail.records) } @@ -601,7 +311,6 @@ public fun last_sequence(trail: &AuditTrail): Option { /// Get a record by sequence number public fun get_record(trail: &AuditTrail, sequence_number: u64): &Record { assert!(linked_table::contains(&trail.records, sequence_number), ERecordNotFound); - linked_table::borrow(&trail.records, sequence_number) } @@ -609,28 +318,3 @@ public fun get_record(trail: &AuditTrail, sequence_number: u public fun has_record(trail: &AuditTrail, sequence_number: u64): bool { linked_table::contains(&trail.records, sequence_number) } - -/// Get the stored data from a record -public fun record_data(record: &Record): &D { - &record.stored_data -} - -/// Get the record metadata -public fun record_metadata(record: &Record): &Option { - &record.record_metadata -} - -/// Get the record sequence number -public fun record_sequence_number(record: &Record): u64 { - record.sequence_number -} - -/// Get who added the record -public fun record_added_by(record: &Record): address { - record.added_by -} - -/// Get when the record was added -public fun record_added_at(record: &Record): u64 { - record.added_at -} diff --git a/audit-trails-move/sources/locking.move b/audit-trails-move/sources/locking.move new file mode 100644 index 0000000..c20ef50 --- /dev/null +++ b/audit-trails-move/sources/locking.move @@ -0,0 +1,103 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Locking configuration for audit trail records +/// +/// Controls when records can be deleted based on time window (records locked for N seconds) +/// or count window (last N records always locked). +module audit_trails::locking; + +/// Controls when records can be deleted (time OR count based) +public struct LockingConfig has copy, drop, store { + /// Records locked for N seconds after creation + time_window_seconds: Option, + /// Last N records are always locked + count_window: Option, +} + +// ===== Constructors ===== + +/// Create a new locking configuration +/// +/// - `time_window_seconds`: Records are locked for N seconds after creation (None = no time lock) +/// - `count_window`: Last N records are always locked (None = no count lock) +public fun new(time_window_seconds: Option, count_window: Option): LockingConfig { + LockingConfig { time_window_seconds, count_window } +} + +/// Create a locking config with no restrictions +public fun none(): LockingConfig { + LockingConfig { + time_window_seconds: option::none(), + count_window: option::none(), + } +} + +/// Create a time-based locking config +public fun time_based(seconds: u64): LockingConfig { + LockingConfig { + time_window_seconds: option::some(seconds), + count_window: option::none(), + } +} + +/// Create a count-based locking config +public fun count_based(count: u64): LockingConfig { + LockingConfig { + time_window_seconds: option::none(), + count_window: option::some(count), + } +} + +// ===== Getters ===== + +/// Get the time window in seconds (if set) +public fun time_window_seconds(config: &LockingConfig): &Option { + &config.time_window_seconds +} + +/// Get the count window (if set) +public fun count_window(config: &LockingConfig): &Option { + &config.count_window +} + +// ===== Locking Logic ===== + +/// Check if a record is locked based on time window +/// +/// Returns true if the record was created within the time window +public fun is_time_locked(config: &LockingConfig, record_timestamp: u64, current_time: u64): bool { + if (config.time_window_seconds.is_none()) { + return false + }; + + let time_window_ms = (*config.time_window_seconds.borrow()) * 1000; + let record_age = current_time - record_timestamp; + record_age < time_window_ms +} + +/// Check if a record is locked based on count window +/// +/// Returns true if the record is among the last N records +public fun is_count_locked(config: &LockingConfig, sequence_number: u64, total_records: u64): bool { + if (config.count_window.is_none()) { + return false + }; + + let count_window = *config.count_window.borrow(); + + let records_after = total_records - sequence_number - 1; + records_after < count_window +} + +/// Check if a record is locked (either by time or count) +public fun is_locked( + config: &LockingConfig, + sequence_number: u64, + record_timestamp: u64, + total_records: u64, + current_time: u64, +): bool { + is_time_locked(config, record_timestamp, current_time) + || is_count_locked(config, sequence_number, total_records) +} diff --git a/audit-trails-move/sources/record.move b/audit-trails-move/sources/record.move new file mode 100644 index 0000000..8f9adf9 --- /dev/null +++ b/audit-trails-move/sources/record.move @@ -0,0 +1,70 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Record module for audit trail entries +/// +/// A Record represents a single entry in an audit trail, stored in a LinkedTable +/// and addressed by trail_id + sequence_number. +module audit_trails::record; + +use std::string::String; + +/// A single record in the audit trail (stored in LinkedTable, no ObjectID) +public struct Record has store { + /// Arbitrary data stored on-chain + stored_data: D, + /// Optional metadata for this specific record + record_metadata: Option, + /// Position in the trail (0-indexed, never reused) + sequence_number: u64, + /// Who added this record + added_by: address, + /// When this record was added (milliseconds) + added_at: u64, +} + +// ===== Constructors ===== + +/// Create a new record (package-private, called by audit_trails module) +public(package) fun new( + stored_data: D, + record_metadata: Option, + sequence_number: u64, + added_by: address, + added_at: u64, +): Record { + Record { + stored_data, + record_metadata, + sequence_number, + added_by, + added_at, + } +} + +// ===== Getters ===== + +/// Get the stored data from a record +public fun data(record: &Record): &D { + &record.stored_data +} + +/// Get the record metadata +public fun metadata(record: &Record): &Option { + &record.record_metadata +} + +/// Get the record sequence number +public fun sequence_number(record: &Record): u64 { + record.sequence_number +} + +/// Get who added the record +public fun added_by(record: &Record): address { + record.added_by +} + +/// Get when the record was added (milliseconds) +public fun added_at(record: &Record): u64 { + record.added_at +} From d1d8dc5a69602b1158461243b1c21f89153ff8d8 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Sun, 21 Dec 2025 16:30:33 +0000 Subject: [PATCH 03/18] First version of permission * First implementation of the permission module based on a `Permission` enum * Unit tests for the `Permission` enum * Renamed `AuditTrail::permissions` to `AuditTrail::roles` * Renamed all modules and type-names from plural to singular name * audit_trails -> audit_trail * permissions -> permission * capabilities -> capability --- .gitignore | 3 +- .../Move.lock | 0 .../Move.toml | 4 +- .../sources/audit_trail.move | 24 +-- .../sources/capability.move | 2 +- .../sources/locking.move | 2 +- audit-trail-move/sources/permission.move | 195 ++++++++++++++++++ .../sources/record.move | 2 +- audit-trail-move/tests/permission_tests.move | 104 ++++++++++ {audit-trails-rs => audit-trail-rs}/README.md | 0 audit-trails-move/sources/permissions.move | 63 ------ 11 files changed, 318 insertions(+), 81 deletions(-) rename {audit-trails-move => audit-trail-move}/Move.lock (100%) rename {audit-trails-move => audit-trail-move}/Move.toml (58%) rename audit-trails-move/sources/audit_trails.move => audit-trail-move/sources/audit_trail.move (93%) rename audit-trails-move/sources/capabilities.move => audit-trail-move/sources/capability.move (95%) rename {audit-trails-move => audit-trail-move}/sources/locking.move (99%) create mode 100644 audit-trail-move/sources/permission.move rename {audit-trails-move => audit-trail-move}/sources/record.move (98%) create mode 100644 audit-trail-move/tests/permission_tests.move rename {audit-trails-rs => audit-trail-rs}/README.md (100%) delete mode 100644 audit-trails-move/sources/permissions.move diff --git a/.gitignore b/.gitignore index d29419b..1fc1232 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ # ignore folder created in CI for downloaded iota binaries /iota/ -/toml-cli/ \ No newline at end of file +/toml-cli/ +/audit-trail-move/build \ No newline at end of file diff --git a/audit-trails-move/Move.lock b/audit-trail-move/Move.lock similarity index 100% rename from audit-trails-move/Move.lock rename to audit-trail-move/Move.lock diff --git a/audit-trails-move/Move.toml b/audit-trail-move/Move.toml similarity index 58% rename from audit-trails-move/Move.toml rename to audit-trail-move/Move.toml index 19af91a..a66b870 100644 --- a/audit-trails-move/Move.toml +++ b/audit-trail-move/Move.toml @@ -1,8 +1,8 @@ [package] -name = "audit_trails" +name = "audit_trail" edition = "2024.beta" [dependencies] [addresses] -audit_trails = "0x0" +audit_trail = "0x0" diff --git a/audit-trails-move/sources/audit_trails.move b/audit-trail-move/sources/audit_trail.move similarity index 93% rename from audit-trails-move/sources/audit_trails.move rename to audit-trail-move/sources/audit_trail.move index 89aebd3..dfc05dd 100644 --- a/audit-trails-move/sources/audit_trails.move +++ b/audit-trail-move/sources/audit_trail.move @@ -1,18 +1,18 @@ // Copyright (c) 2025 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -/// Audit trails with role-based access control and timelock +/// Audit Trails with role-based access control and timelock /// /// An audit trail is a tamper-proof, sequential chain of notarized records where each entry /// references its predecessor, ensuring verifiable continuity and integrity. /// /// Records are addressed by trail_id + sequence_number -module audit_trails::audit_trails; +module audit_trail::main; -use audit_trails::capabilities::{Self, Capability}; -use audit_trails::locking::{Self, LockingConfig}; -use audit_trails::permissions::{Self, Permission}; -use audit_trails::record::{Self, Record}; +use audit_trail::capability::{Self, Capability}; +use audit_trail::locking::{Self, LockingConfig}; +use audit_trail::permission::{Self, Permission}; +use audit_trail::record::{Self, Record}; use iota::clock::{Self, Clock}; use iota::event; use iota::linked_table::{Self, LinkedTable}; @@ -46,14 +46,14 @@ public struct AuditTrail has key, store { records: LinkedTable>, /// Deletion locking rules locking_config: LockingConfig, - /// Role name -> set of permissions (TODO: implement) - permissions: VecMap>, + /// A list of role definitions consisting of a unique role specifier and a list of associated permissions + roles: VecMap>, /// Set at creation, cannot be changed immutable_metadata: TrailImmutableMetadata, /// Can be updated by holders of MetadataUpdate permission updatable_metadata: Option, /// Whitelist of all issued capability IDs (TODO: implement) - issued_capabilities: VecSet, + issued_capability: VecSet, } // ===== Events ===== @@ -131,7 +131,7 @@ public fun create( // TODO: Initialize setup role with admin permissions (bootstrap) // The creator should receive a setup capability with PermissionAdmin + CapAdmin - // to configure roles and issue capabilities to other users. + // to configure roles and issue capability to other users. let trail = AuditTrail { id: trail_uid, @@ -140,10 +140,10 @@ public fun create( record_count, records, locking_config, - permissions: vec_map::empty(), + roles: vec_map::empty(), immutable_metadata: trail_metadata, updatable_metadata, - issued_capabilities: iota::vec_set::empty(), + issued_capability: iota::vec_set::empty(), }; transfer::share_object(trail); diff --git a/audit-trails-move/sources/capabilities.move b/audit-trail-move/sources/capability.move similarity index 95% rename from audit-trails-move/sources/capabilities.move rename to audit-trail-move/sources/capability.move index fe49bc5..ebaa4e8 100644 --- a/audit-trails-move/sources/capabilities.move +++ b/audit-trail-move/sources/capability.move @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /// Role-based access control capabilities for audit trails -module audit_trails::capabilities; +module audit_trail::capability; /// Capability granting role-based access to an audit trail public struct Capability has key, store { diff --git a/audit-trails-move/sources/locking.move b/audit-trail-move/sources/locking.move similarity index 99% rename from audit-trails-move/sources/locking.move rename to audit-trail-move/sources/locking.move index c20ef50..1946cf9 100644 --- a/audit-trails-move/sources/locking.move +++ b/audit-trail-move/sources/locking.move @@ -5,7 +5,7 @@ /// /// Controls when records can be deleted based on time window (records locked for N seconds) /// or count window (last N records always locked). -module audit_trails::locking; +module audit_trail::locking; /// Controls when records can be deleted (time OR count based) public struct LockingConfig has copy, drop, store { diff --git a/audit-trail-move/sources/permission.move b/audit-trail-move/sources/permission.move new file mode 100644 index 0000000..6fa28c3 --- /dev/null +++ b/audit-trail-move/sources/permission.move @@ -0,0 +1,195 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Permission system for role-based access control +module audit_trail::permission; + +use iota::vec_set::{Self, VecSet}; + +/// Existing permissions for the Audit Trail object +public enum Permission has copy, drop, store { + // --- Whole AUdit TRail related - Proposed role: `Admin` --- + /// Destroy the whole Audit Trail object + AuditTrailDelete, + + // --- Record Management - Proposed role: `RecordAdmin` --- + /// Add records to the trail + RecordAdd, + /// Delete records from the trail + RecordDelete, + /// Correct existing records in the trail + RecordCorrect, // TODO: Clarify if needed for MVP + + + // --- Locking Config - Proposed role: `LockingAdmin` --- + /// Edit the delete_lock configuration for records + RecordDeleteLockConfig, + /// Edit the delete_lock configuration for the whole Audit Trail + TrailDeleteLockConfig, + + + // --- Role Management - Proposed role: `RoleAdmin` --- + /// Add new roles with associated permissions + RolesAdd, + /// Update permissions associated with existing roles + RolesUpdate, + /// Delete existing roles + RolesDelete, + + // --- Capability Management - Proposed role: `CapAdmin` --- + /// Issue new capabilities + CapabilitiesAdd, + /// Revoke existing capabilities + CapabilitiesRevoke, + + // --- Meta Data related - Proposed role: `MetaDataAdmin` --- + /// Update the updatable metadata field + MetaDataUpdate, + /// Delete the updatable metadata field + MetaDataDelete, +} + +/// Create an empty permission set +public fun empty(): VecSet { + vec_set::empty() +} + +/// Add a permission to a set +public fun add(set: &mut VecSet, perm: Permission) { + vec_set::insert(set, perm); +} + +/// Create a permission set from a vector +public fun from_vec(perms: vector): VecSet { + let mut set = vec_set::empty(); + let mut i = 0; + let len = perms.length(); + while (i < len) { + vec_set::insert(&mut set, perms[i]); + i = i + 1; + }; + set +} + +/// Check if a set contains a specific permission +public fun has_permission(set: &VecSet, perm: &Permission): bool { + vec_set::contains(set, perm) +} + +// --------------------------- Functions creating permission sets for often used roles --------------------------- + +/// Create permissions typical used for the `Admin` rolepermissions +public fun admin_permissions(): VecSet { + let mut perms = vec_set::empty(); + perms.insert(audit_trail_delete()); + perms +} + +/// Create permissions typical used for the `RecordAdmin` role +public fun record_admin_permissions(): VecSet { + let mut perms = vec_set::empty(); + perms.insert(record_add()); + perms.insert(record_delete()); + perms.insert(record_correct()); + perms +} + +/// Create permissions typical used for the `LockingAdmin` role +public fun locking_admin_permissions(): VecSet { + let mut perms = vec_set::empty(); + perms.insert(record_delete_lock_config()); + perms.insert(trail_delete_lock_config()); + perms +} + +/// Create permissions typical used for the `RoleAdmin` role +public fun role_admin_permissions(): VecSet { + let mut perms = vec_set::empty(); + perms.insert(roles_add()); + perms.insert(roles_update()); + perms.insert(roles_delete()); + perms +} + +/// Create permissions typical used for the `CapAdmin` role +public fun cap_admin_permissions(): VecSet { + let mut perms = vec_set::empty(); + perms.insert(capabilities_add()); + perms.insert(capabilities_revoke()); + perms +} + +/// Create permissions typical used for the `MetaDataAdmin` role +public fun metadata_admin_permissions(): VecSet { + let mut perms = vec_set::empty(); + perms.insert(meta_data_update()); + perms.insert(meta_data_delete()); + perms +} + +// --------------------------- Constructor functions for all Permission variants --------------------------- + +/// Returns a permission allowing to destroy the whole Audit Trail object +public fun audit_trail_delete(): Permission { + Permission::AuditTrailDelete +} + +/// Returns a permission allowing to add records to the trail +public fun record_add(): Permission { + Permission::RecordAdd +} + +/// Returns a permission allowing to delete records from the trail +public fun record_delete(): Permission { + Permission::RecordDelete +} + +/// Returns a permission allowing to correct existing records in the trail +public fun record_correct(): Permission { + Permission::RecordCorrect +} + +/// Returns a permission allowing to edit the delete_lock configuration for records +public fun record_delete_lock_config(): Permission { + Permission::RecordDeleteLockConfig +} + +/// Returns a permission allowing to edit the delete_lock configuration for the whole Audit Trail +public fun trail_delete_lock_config(): Permission { + Permission::TrailDeleteLockConfig +} + +/// Returns a permission allowing to add new roles with associated permissions +public fun roles_add(): Permission { + Permission::RolesAdd +} + +/// Returns a permission allowing to update permissions associated with existing roles +public fun roles_update(): Permission { + Permission::RolesUpdate +} + +/// Returns a permission allowing to delete existing roles +public fun roles_delete(): Permission { + Permission::RolesDelete +} + +/// Returns a permission allowing to issue new capabilities +public fun capabilities_add(): Permission { + Permission::CapabilitiesAdd +} + +/// Returns a permission allowing to revoke existing capabilities +public fun capabilities_revoke(): Permission { + Permission::CapabilitiesRevoke +} + +/// Returns a permission allowing to update the updatable_metadata field +public fun meta_data_update(): Permission { + Permission::MetaDataUpdate +} + +/// Returns a permission allowing to delete the updatable_metadata field +public fun meta_data_delete(): Permission { + Permission::MetaDataDelete +} diff --git a/audit-trails-move/sources/record.move b/audit-trail-move/sources/record.move similarity index 98% rename from audit-trails-move/sources/record.move rename to audit-trail-move/sources/record.move index 8f9adf9..06956cc 100644 --- a/audit-trails-move/sources/record.move +++ b/audit-trail-move/sources/record.move @@ -5,7 +5,7 @@ /// /// A Record represents a single entry in an audit trail, stored in a LinkedTable /// and addressed by trail_id + sequence_number. -module audit_trails::record; +module audit_trail::record; use std::string::String; diff --git a/audit-trail-move/tests/permission_tests.move b/audit-trail-move/tests/permission_tests.move new file mode 100644 index 0000000..ac13674 --- /dev/null +++ b/audit-trail-move/tests/permission_tests.move @@ -0,0 +1,104 @@ +#[test_only] +module audit_trail::permission_tests; + +use audit_trail::permission::{Self}; +use iota::vec_set; + +#[test] +fun test_has_permission_empty_set() { + let set = permission::empty(); + assert!(vec_set::size(&set) == 0, 0); +} + +#[test] +fun test_has_permission_single_permission() { + let mut set = permission::empty(); + let perm = permission::record_add(); + permission::add(&mut set, perm); + + assert!(permission::has_permission(&set, &perm), 0); +} + +#[test] +fun test_has_permission_not_in_set() { + let mut set = permission::empty(); + permission::add(&mut set, permission::record_add()); + + let perm = permission::record_delete(); + assert!(!permission::has_permission(&set, &perm), 0); +} + +#[test] +fun test_has_permission_multiple_permission() { + let mut set = permission::empty(); + permission::add(&mut set, permission::record_add()); + permission::add(&mut set, permission::record_delete()); + permission::add(&mut set, permission::audit_trail_delete()); + + assert!(permission::has_permission(&set, &permission::record_add()), 0); + assert!(permission::has_permission(&set, &permission::record_delete()), 0); + assert!(permission::has_permission(&set, &permission::audit_trail_delete()), 0); + assert!(!permission::has_permission(&set, &permission::record_correct()), 0); +} + +#[test] +fun test_has_permission_from_vec() { + let perms = vector[ + permission::record_add(), + permission::record_delete(), + permission::meta_data_update(), + ]; + let set = permission::from_vec(perms); + + assert!(permission::has_permission(&set, &permission::record_add()), 0); + assert!(permission::has_permission(&set, &permission::record_delete()), 0); + assert!(permission::has_permission(&set, &permission::meta_data_update()), 0); + assert!(!permission::has_permission(&set, &permission::audit_trail_delete()), 0); +} + +#[test] +fun test_from_vec_empty() { + let perms = vector[]; + let set = permission::from_vec(perms); + + assert!(vec_set::size(&set) == 0, 0); +} + +#[test] +fun test_from_vec_single_permission() { + let perms = vector[permission::record_add()]; + let set = permission::from_vec(perms); + + assert!(vec_set::size(&set) == 1, 0); + assert!(permission::has_permission(&set, &permission::record_add()), 0); +} + +#[test] +fun test_from_vec_multiple_permission() { + let perms = vector[ + permission::record_add(), + permission::record_delete(), + permission::audit_trail_delete(), + ]; + let set = permission::from_vec(perms); + + assert!(vec_set::size(&set) == 3, 0); + assert!(permission::has_permission(&set, &permission::record_add()), 0); + assert!(permission::has_permission(&set, &permission::record_delete()), 0); + assert!(permission::has_permission(&set, &permission::audit_trail_delete()), 0); + assert!(!permission::has_permission(&set, &permission::record_correct()), 0); +} + +#[test] +#[expected_failure(abort_code = vec_set::EKeyAlreadyExists)] +fun test_from_vec_duplicate_permission() { + // VecSet should throw error EKeyAlreadyExists on duplicate insertions + let perms = vector[ + permission::record_add(), + permission::record_delete(), + permission::record_add(), // duplicate + ]; + let set = permission::from_vec(perms); + // The following line should not be reached due to the expected failure + assert!(vec_set::size(&set) == 2, 0); +} \ No newline at end of file diff --git a/audit-trails-rs/README.md b/audit-trail-rs/README.md similarity index 100% rename from audit-trails-rs/README.md rename to audit-trail-rs/README.md diff --git a/audit-trails-move/sources/permissions.move b/audit-trails-move/sources/permissions.move deleted file mode 100644 index f76d714..0000000 --- a/audit-trails-move/sources/permissions.move +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2025 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/// Permission system for role-based access control -module audit_trails::permissions; - -use iota::vec_set::{Self, VecSet}; - -public struct Permission has copy, drop, store { - value: u8, -} - -/// Create an empty permission set -public fun empty(): VecSet { - vec_set::empty() -} - -/// Add a permission to a set -public fun add(set: &mut VecSet, perm: Permission) { - vec_set::insert(set, perm); -} - -/// Create a permission set from a vector -public fun from_vec(perms: vector): VecSet { - let mut set = vec_set::empty(); - let mut i = 0; - let len = perms.length(); - while (i < len) { - vec_set::insert(&mut set, perms[i]); - i = i + 1; - }; - set -} - -/// Check if a set contains a specific permission -public fun has_permission(set: &VecSet, perm: &Permission): bool { - vec_set::contains(set, perm) -} - -/// Permission to manage roles and permissions -public fun permission_admin(): Permission { - Permission { value: 0 } -} - -/// Permission to issue/revoke capabilities -public fun cap_admin(): Permission { - Permission { value: 0 } -} - -/// Permission to add records to the trail -public fun record_add(): Permission { - Permission { value: 0 } -} - -/// Permission to update trail metadata -public fun metadata_update(): Permission { - Permission { value: 0 } -} - -/// Permission to update locking configuration -public fun locking_update(): Permission { - Permission { value: 0 } -} From 632d1e785084f84518e5e794acbf83ef9269fb16 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Mon, 22 Dec 2025 08:20:43 +0000 Subject: [PATCH 04/18] First version of initial roles config and Admin role Capability creation Unit tests are still buggy and will be fixed with the next commit. --- audit-trail-move/sources/audit_trail.move | 208 ++++++++++++++++++++-- audit-trail-move/sources/capability.move | 62 +++++-- audit-trail-move/sources/permission.move | 5 + 3 files changed, 245 insertions(+), 30 deletions(-) diff --git a/audit-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move index dfc05dd..5bdba96 100644 --- a/audit-trail-move/sources/audit_trail.move +++ b/audit-trail-move/sources/audit_trail.move @@ -17,12 +17,21 @@ use iota::clock::{Self, Clock}; use iota::event; use iota::linked_table::{Self, LinkedTable}; use iota::vec_map::{Self, VecMap}; -use iota::vec_set::VecSet; +use iota::vec_set::{Self, VecSet}; use std::string::String; // ===== Errors ===== #[error] const ERecordNotFound: vector = b"Record not found at the given sequence number"; +#[error] +const ERoleDoesNotExist: vector = b"The specified role does not exist in the roles map"; +#[error] +const EPermissionDenied: vector = b"The role associated with the provided capability does not have the required permission"; +#[error] +const ETrailIdNotCorrect: vector = b"The trail ID associated with the provided capability does not match the audit trail"; + +// ===== Constants ===== +const INITIAL_ADMIN_ROLE_NAME: vector = b"Admin"; // ===== Core Structures ===== @@ -66,6 +75,8 @@ public struct AuditTrailCreated has copy, drop { has_initial_record: bool, } +// TODO: Add event for trail deletion + /// Emitted when a record is added to the trail /// Records are identified by trail_id + sequence_number public struct RecordAdded has copy, drop { @@ -75,6 +86,19 @@ public struct RecordAdded has copy, drop { timestamp: u64, } +// TODO: Add event for Record deletion and (if part of MVP) correction + +/// Emitted when a capability is issued +public struct CapabilityIssued has copy, drop { + trail_id: ID, + capability_id: ID, + role: String, + issued_to: address, + issued_by: address, + timestamp: u64, +} + + // ===== Constructors ===== /// Create immutable trail metadata @@ -88,6 +112,22 @@ public fun new_trail_metadata( // ===== Trail Creation ===== /// Create a new audit trail with optional initial record +/// +/// Initial roles config +/// -------------------- +/// Initializes the `roles` map with only one role, called "Admin" which is associated with the permissions +/// * TrailDelete +/// * CapabilitiesAdd +/// * CapabilitiesRevoke +/// * RolesAdd +/// * RolesUpdate +/// * RolesDelete +/// +/// Returns +/// ------- +/// * Capability with "Admin" role, allowing the creator to define custom +/// roles and issue capabilities to other users. +/// * Trail ID public fun create( initial_data: Option, initial_record_metadata: Option, @@ -96,7 +136,7 @@ public fun create( updatable_metadata: Option, clock: &Clock, ctx: &mut TxContext, -): ID { +): (Capability, ID) { let creator = ctx.sender(); let timestamp = clock::timestamp_ms(clock); @@ -129,9 +169,8 @@ public fun create( initial_data.destroy_none(); }; - // TODO: Initialize setup role with admin permissions (bootstrap) - // The creator should receive a setup capability with PermissionAdmin + CapAdmin - // to configure roles and issue capability to other users. + let mut roles = vec_map::empty>(); + roles.insert(initial_admin_role_name(), permission::admin_permissions()); let trail = AuditTrail { id: trail_uid, @@ -140,7 +179,7 @@ public fun create( record_count, records, locking_config, - roles: vec_map::empty(), + roles, immutable_metadata: trail_metadata, updatable_metadata, issued_capability: iota::vec_set::empty(), @@ -148,6 +187,12 @@ public fun create( transfer::share_object(trail); + let admin_cap = capability::new_capability( + initial_admin_role_name(), + trail_id, + ctx, + ); + event::emit(AuditTrailCreated { trail_id, creator, @@ -155,7 +200,11 @@ public fun create( has_initial_record, }); - trail_id + (admin_cap, trail_id) +} + +public fun initial_admin_role_name(): String { + INITIAL_ADMIN_ROLE_NAME.to_string() } // ===== Record Operations ===== @@ -252,17 +301,17 @@ public fun update_metadata( // ===== Trail Query Functions ===== /// Get the total number of records in the trail -public fun record_count(trail: &AuditTrail): u64 { +public fun trail_record_count(trail: &AuditTrail): u64 { trail.record_count } /// Get the trail creator address -public fun creator(trail: &AuditTrail): address { +public fun trail_creator(trail: &AuditTrail): address { trail.creator } /// Get the trail creation timestamp -public fun created_at(trail: &AuditTrail): u64 { +public fun trail_created_at(trail: &AuditTrail): u64 { trail.created_at } @@ -272,49 +321,170 @@ public fun trail_id(trail: &AuditTrail): ID { } /// Get the trail name (immutable metadata) -public fun name(trail: &AuditTrail): &Option { +public fun trail_name(trail: &AuditTrail): &Option { &trail.immutable_metadata.name } /// Get the trail description (immutable metadata) -public fun description(trail: &AuditTrail): &Option { +public fun trail_description(trail: &AuditTrail): &Option { &trail.immutable_metadata.description } /// Get the updatable metadata -public fun metadata(trail: &AuditTrail): &Option { +public fun trail_metadata(trail: &AuditTrail): &Option { &trail.updatable_metadata } /// Get the locking configuration -public fun locking_config(trail: &AuditTrail): &LockingConfig { +public fun trail_locking_config(trail: &AuditTrail): &LockingConfig { &trail.locking_config } /// Check if the trail is empty (no records) -public fun is_empty(trail: &AuditTrail): bool { +public fun trail_is_empty(trail: &AuditTrail): bool { linked_table::is_empty(&trail.records) } /// Get the first sequence number (None if empty) -public fun first_sequence(trail: &AuditTrail): Option { +public fun trail_first_sequence(trail: &AuditTrail): Option { *linked_table::front(&trail.records) } /// Get the last sequence number (None if empty) -public fun last_sequence(trail: &AuditTrail): Option { +public fun trail_last_sequence(trail: &AuditTrail): Option { *linked_table::back(&trail.records) } // ===== Record Query Functions ===== /// Get a record by sequence number -public fun get_record(trail: &AuditTrail, sequence_number: u64): &Record { +public fun trail_get_record(trail: &AuditTrail, sequence_number: u64): &Record { assert!(linked_table::contains(&trail.records, sequence_number), ERecordNotFound); linked_table::borrow(&trail.records, sequence_number) } /// Check if a record exists at the given sequence number -public fun has_record(trail: &AuditTrail, sequence_number: u64): bool { +public fun trail_has_record(trail: &AuditTrail, sequence_number: u64): bool { linked_table::contains(&trail.records, sequence_number) } + +// ===== Role and Capability related Functions ===== + +/// Get the permissions associated with a specific role. +/// Aborts with ERoleDoesNotExist if the role does not exist. +public fun trail_get_role_permissions( + trail: &AuditTrail, + role: &String, +): &VecSet { + assert!(vec_map::contains(&trail.roles, role), ERoleDoesNotExist); + vec_map::get(&trail.roles, role) +} + +/// Indicates if a provided capability has a specific permission. +public fun trail_has_capability_permission( + trail: &AuditTrail, + cap: &Capability, + permission: &Permission, +): bool { + assert!(trail.id() == cap.trail_id(), ETrailIdNotCorrect); + let permissions = trail.get_role_permissions(cap.role()); + vec_set::contains(permissions, permission) +} + +/// Create a new capability with a specific role +public fun trail_new_capability( + trail: &mut AuditTrail, + cap: &Capability, + role: &String, + ctx: &mut TxContext, +): Capability { + assert!(trail.has_capability_permission(cap, &permission::capabilities_add()), EPermissionDenied); + capability::new_capability( + *role, + trail.id(), + ctx, + ) +} + +/// Destroy an existing capability +/// Every owner of a capability is allowed to destroy it when no longer needed. +/// TODO: Clarify if we need to restrict access with the `CapabilitiesRevoke` permission here. +/// If yes, we also need a destroy function for Admin capabilities (without the need of another Admin capability). +/// Otherwise the last Admin capability holder will block the trail forever by not being able to destroy it. +public fun trail_destroy_capability( + trail: &mut AuditTrail, + cap_to_destroy: Capability, +) { + assert!(trail.id() == cap_to_destroy.trail_id(), ETrailIdNotCorrect); + // TODO: Implement revocation logic (e.g., remove from issued_capability set) + cap_to_destroy.destroy(); +} + +public fun trail_revoke_capability( + trail: &mut AuditTrail, + cap: &Capability, + cap_to_revoke: ID, +) { + assert!(trail.has_capability_permission(cap, &permission::capabilities_revoke()), EPermissionDenied); + // TODO: Implement revocation logic (e.g., remove from issued_capability set) +} + +/// Create a new role consisting of a role name and associated permissions +public fun trail_create_role( + trail: &mut AuditTrail, + cap: &Capability, + role: String, + permissions: VecSet, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::roles_add()), EPermissionDenied); + vec_map::insert(&mut trail.roles, role, permissions); +} + +/// Delete an existing role +public fun trail_delete_role( + trail: &mut AuditTrail, + cap: &Capability, + role: &String, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::roles_delete()), EPermissionDenied); + vec_map::remove(&mut trail.roles, role); +} + +/// Update permissions associated with an existing role +public fun trail_update_role_permissions( + trail: &mut AuditTrail, + cap: &Capability, + role: &String, + new_permissions: VecSet, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::roles_update()), EPermissionDenied); + assert!(vec_map::contains(&trail.roles, role), ERoleDoesNotExist); + vec_map::insert(&mut trail.roles, *role, new_permissions); +} + +// ===== public use statements ===== + +public use fun trail_id as AuditTrail.id; +public use fun trail_creator as AuditTrail.creator; +public use fun trail_created_at as AuditTrail.created_at; +public use fun trail_record_count as AuditTrail.record_count; +public use fun trail_name as AuditTrail.name; +public use fun trail_description as AuditTrail.description; +public use fun trail_metadata as AuditTrail.metadata; +public use fun trail_locking_config as AuditTrail.locking_config; +public use fun trail_is_empty as AuditTrail.is_empty; +public use fun trail_first_sequence as AuditTrail.first_sequence; +public use fun trail_last_sequence as AuditTrail.last_sequence; +public use fun trail_get_record as AuditTrail.get_record; +public use fun trail_has_record as AuditTrail.has_record; +public use fun trail_get_role_permissions as AuditTrail.get_role_permissions; +public use fun trail_has_capability_permission as AuditTrail.has_capability_permission; +public use fun trail_new_capability as AuditTrail.new_capability; +public use fun trail_destroy_capability as AuditTrail.destroy_capability; +public use fun trail_revoke_capability as AuditTrail.revoke_capability; +public use fun trail_create_role as AuditTrail.create_role; +public use fun trail_delete_role as AuditTrail.delete_role; +public use fun trail_update_role_permissions as AuditTrail.update_role_permissions; diff --git a/audit-trail-move/sources/capability.move b/audit-trail-move/sources/capability.move index ebaa4e8..cbc965b 100644 --- a/audit-trail-move/sources/capability.move +++ b/audit-trail-move/sources/capability.move @@ -4,32 +4,72 @@ /// Role-based access control capabilities for audit trails module audit_trail::capability; +use std::string::String; + +// ===== Core Structures ===== + /// Capability granting role-based access to an audit trail public struct Capability has key, store { id: UID, -} - -/// Create a setup capability for trail initialization -public fun new_setup_cap(ctx: &mut TxContext): Capability { - Capability { - id: object::new(ctx), - } + trail_id: ID, + role: String } /// Create a new capability with a specific role -public fun new_capability(ctx: &mut TxContext): Capability { - Capability { +public(package) fun new_capability( + role: String, + trail_id: ID, + ctx: &mut TxContext, +): Capability { + Capability { id: object::new(ctx), + role, + trail_id, } } + +// TODO: Is this needed? What is a setup capability? +// +// /// Create a setup capability for trail initialization +// public fun new_setup_cap(ctx: &mut TxContext): Capability { +// Capability { +// id: object::new(ctx), +// } +// } + + + /// Get the capability's ID public fun cap_id(cap: &Capability): ID { object::uid_to_inner(&cap.id) } +/// Get the capability's role +public fun cap_role(cap: &Capability): &String { + &cap.role +} + +/// Get the capability's trail ID +public fun cap_trail_id(cap: &Capability): ID { + cap.trail_id +} + +/// Check if the capability has a specific role +public fun cap_has_role(cap: &Capability, role: &String): bool { + &cap.role == role +} + /// Destroy a capability -public fun destroy_capability(cap: Capability) { - let Capability { id } = cap; +public(package) fun cap_destroy(cap: Capability) { + let Capability { id, role: _role, trail_id: _trail_id } = cap; object::delete(id); } + +// ===== public use statements ===== + +public use fun cap_id as Capability.id; +public use fun cap_role as Capability.role; +public use fun cap_trail_id as Capability.trail_id; +public use fun cap_has_role as Capability.has_role; +public use fun cap_destroy as Capability.destroy; \ No newline at end of file diff --git a/audit-trail-move/sources/permission.move b/audit-trail-move/sources/permission.move index 6fa28c3..71d34c2 100644 --- a/audit-trail-move/sources/permission.move +++ b/audit-trail-move/sources/permission.move @@ -82,6 +82,11 @@ public fun has_permission(set: &VecSet, perm: &Permission): bool { public fun admin_permissions(): VecSet { let mut perms = vec_set::empty(); perms.insert(audit_trail_delete()); + perms.insert(capabilities_add()); + perms.insert(capabilities_revoke()); + perms.insert(roles_add()); + perms.insert(roles_update()); + perms.insert(roles_delete()); perms } From f5b6b2a9e69196910a1355c1967bbfaa7a7fa257 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Mon, 22 Dec 2025 10:55:20 +0000 Subject: [PATCH 05/18] Add missing tests for create AT --- audit-trail-move/tests/create_tests.move | 287 +++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 audit-trail-move/tests/create_tests.move diff --git a/audit-trail-move/tests/create_tests.move b/audit-trail-move/tests/create_tests.move new file mode 100644 index 0000000..046de20 --- /dev/null +++ b/audit-trail-move/tests/create_tests.move @@ -0,0 +1,287 @@ +#[test_only] +module audit_trail::create_tests; + +use audit_trail::main::{Self, AuditTrail, initial_admin_role_name}; +use audit_trail::locking::{Self}; +use audit_trail::capability::{Capability}; +use iota::test_scenario::{Self as ts}; +use iota::clock::{Self}; +use std::string::{Self}; + +/// Test data type for audit trail records +public struct TestData has store, copy, drop { + value: u64, + message: vector, +} + +fun destroy_capability(admin_cap: Capability, scenario: &ts::Scenario) { + let mut trail = ts::take_shared>(scenario); + trail.destroy_capability( admin_cap); + ts::return_shared(trail); +} + +#[test] +fun test_create_without_initial_record() { + let user = @0xA; + let mut scenario = ts::begin(user); + + { + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(1000); + + let locking_config = locking::new(std::option::none(), std::option::some(0)); + let trail_metadata = main::new_trail_metadata( + std::option::some(string::utf8(b"Test Trail")), + std::option::some(string::utf8(b"A test audit trail")), + ); + + let (admin_cap, trail_id) = main::create( + std::option::none(), + std::option::none(), + locking_config, + trail_metadata, + std::option::some(string::utf8(b"Updatable metadata")), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability was created + assert!(admin_cap.role() == initial_admin_role_name(), 0); + assert!(admin_cap.trail_id() == trail_id, 1); + + // Clean up + clock::destroy_for_testing(clock); + destroy_capability(admin_cap, &scenario); + }; + + ts::next_tx(&mut scenario, user); + { + let trail = ts::take_shared>(&scenario); + + // Verify trail was created correctly + assert!(trail.trail_creator() == user, 2); + assert!(trail.trail_created_at() == 1000, 3); + assert!(trail.trail_record_count() == 0, 4); + + ts::return_shared(trail); + }; + + ts::end(scenario); +} + +#[test] +fun test_create_with_initial_record() { + let user = @0xB; + let mut scenario = ts::begin(user); + + { + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(2000); + + let locking_config = locking::new(std::option::some(86400), std::option::none()); // 1 day in seconds + let trail_metadata = main::new_trail_metadata( + std::option::some(string::utf8(b"Test Trail with Record")), + std::option::some(string::utf8(b"A test audit trail with initial record")), + ); + + let initial_data = TestData { + value: 42, + message: b"Hello, World!", + }; + + let (admin_cap, trail_id) = main::create( + std::option::some(initial_data), + std::option::some(string::utf8(b"Initial record metadata")), + locking_config, + trail_metadata, + std::option::some(string::utf8(b"Updatable metadata")), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability + assert!(admin_cap.role() == initial_admin_role_name(), 0); + assert!(admin_cap.trail_id() == trail_id, 1); + + // Clean up + clock::destroy_for_testing(clock); + destroy_capability(admin_cap, &scenario); + }; + + ts::next_tx(&mut scenario, user); + { + let trail = ts::take_shared>(&scenario); + + // Verify trail with initial record + assert!(trail.trail_creator() == user, 2); + assert!(trail.trail_created_at() == 2000, 3); + assert!(trail.trail_record_count() == 1, 4); + + // Verify the initial record exists + assert!(trail.trail_has_record(0), 5); + + ts::return_shared(trail); + }; + + ts::end(scenario); +} + +#[test] +fun test_create_minimal_metadata() { + let user = @0xC; + let mut scenario = ts::begin(user); + + { + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(3000); + + let locking_config = locking::new(std::option::none(), std::option::some(0)); + let trail_metadata = main::new_trail_metadata( + std::option::none(), + std::option::none(), + ); + + let (admin_cap, _trail_id) = main::create( + std::option::none(), + std::option::none(), + locking_config, + trail_metadata, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability was created + assert!(admin_cap.role() == initial_admin_role_name(), 0); + + // Clean up + destroy_capability(admin_cap, &scenario); + clock::destroy_for_testing(clock); + }; + + ts::next_tx(&mut scenario, user); + { + let trail = ts::take_shared>(&scenario); + + // Verify trail was created + assert!(trail.trail_creator() == user, 1); + assert!(trail.trail_created_at() == 3000, 2); + assert!(trail.trail_record_count() == 0, 3); + + ts::return_shared(trail); + }; + + ts::end(scenario); +} + +#[test] +fun test_create_with_locking_enabled() { + let user = @0xD; + let mut scenario = ts::begin(user); + + { + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(4000); + + let locking_config = locking::new(std::option::some(604800), std::option::none()); // 7 days in seconds + let trail_metadata = main::new_trail_metadata( + std::option::some(string::utf8(b"Locked Trail")), + std::option::none(), + ); + + let (admin_cap, _trail_id) = main::create( + std::option::none(), + std::option::none(), + locking_config, + trail_metadata, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + // Clean up + destroy_capability(admin_cap, &scenario); + clock::destroy_for_testing(clock); + }; + + ts::next_tx(&mut scenario, user); + { + let trail = ts::take_shared>(&scenario); + + // Verify trail with locking enabled + assert!(trail.trail_creator() == user, 0); + assert!(trail.trail_record_count() == 0, 1); + + ts::return_shared(trail); + }; + + ts::end(scenario); +} + +#[test] +fun test_create_multiple_trails() { + let user = @0xE; + let mut scenario = ts::begin(user); + + let mut trail_ids = vector::empty(); + + // Create first trail + { + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(5000); + + let locking_config = locking::new(std::option::none(), std::option::some(0)); + let trail_metadata = main::new_trail_metadata( + std::option::some(string::utf8(b"Trail 1")), + std::option::none(), + ); + + let (admin_cap1, trail_id1) = main::create( + std::option::none(), + std::option::none(), + locking_config, + trail_metadata, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + trail_ids.push_back(trail_id1); + ts::return_to_sender(&scenario, admin_cap1); + clock::destroy_for_testing(clock); + }; + + ts::next_tx(&mut scenario, user); + + // Create second trail + { + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(6000); + + let locking_config = locking::new(std::option::none(), std::option::some(0)); + let trail_metadata = main::new_trail_metadata( + std::option::some(string::utf8(b"Trail 2")), + std::option::none(), + ); + + let (admin_cap2, trail_id2) = main::create( + std::option::none(), + std::option::none(), + locking_config, + trail_metadata, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + trail_ids.push_back(trail_id2); + + // Verify trails have different IDs + assert!(trail_ids[0] != trail_ids[1], 0); + + ts::return_to_sender(&scenario, admin_cap2); + clock::destroy_for_testing(clock); + }; + + ts::end(scenario); +} From 395a68f028a49e7bb27105379472642ac62e726c Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Mon, 22 Dec 2025 11:20:31 +0000 Subject: [PATCH 06/18] Fixes for the create AT tests --- audit-trail-move/sources/capability.move | 11 ++++++++--- audit-trail-move/tests/create_tests.move | 18 ++++++------------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/audit-trail-move/sources/capability.move b/audit-trail-move/sources/capability.move index cbc965b..7cd6a81 100644 --- a/audit-trail-move/sources/capability.move +++ b/audit-trail-move/sources/capability.move @@ -38,8 +38,6 @@ public(package) fun new_capability( // } // } - - /// Get the capability's ID public fun cap_id(cap: &Capability): ID { object::uid_to_inner(&cap.id) @@ -66,10 +64,17 @@ public(package) fun cap_destroy(cap: Capability) { object::delete(id); } +#[test_only] +public fun cap_destroy_for_testing(cap: Capability) { + cap_destroy(cap); +} + // ===== public use statements ===== public use fun cap_id as Capability.id; public use fun cap_role as Capability.role; public use fun cap_trail_id as Capability.trail_id; public use fun cap_has_role as Capability.has_role; -public use fun cap_destroy as Capability.destroy; \ No newline at end of file +public use fun cap_destroy as Capability.destroy; +#[test_only] +public use fun cap_destroy_for_testing as Capability.destroy_for_testing; \ No newline at end of file diff --git a/audit-trail-move/tests/create_tests.move b/audit-trail-move/tests/create_tests.move index 046de20..e29f8d4 100644 --- a/audit-trail-move/tests/create_tests.move +++ b/audit-trail-move/tests/create_tests.move @@ -14,12 +14,6 @@ public struct TestData has store, copy, drop { message: vector, } -fun destroy_capability(admin_cap: Capability, scenario: &ts::Scenario) { - let mut trail = ts::take_shared>(scenario); - trail.destroy_capability( admin_cap); - ts::return_shared(trail); -} - #[test] fun test_create_without_initial_record() { let user = @0xA; @@ -51,7 +45,7 @@ fun test_create_without_initial_record() { // Clean up clock::destroy_for_testing(clock); - destroy_capability(admin_cap, &scenario); + admin_cap.destroy_for_testing(); }; ts::next_tx(&mut scenario, user); @@ -105,7 +99,7 @@ fun test_create_with_initial_record() { // Clean up clock::destroy_for_testing(clock); - destroy_capability(admin_cap, &scenario); + admin_cap.destroy_for_testing(); }; ts::next_tx(&mut scenario, user); @@ -155,7 +149,7 @@ fun test_create_minimal_metadata() { assert!(admin_cap.role() == initial_admin_role_name(), 0); // Clean up - destroy_capability(admin_cap, &scenario); + admin_cap.destroy_for_testing(); clock::destroy_for_testing(clock); }; @@ -200,7 +194,7 @@ fun test_create_with_locking_enabled() { ); // Clean up - destroy_capability(admin_cap, &scenario); + admin_cap.destroy_for_testing(); clock::destroy_for_testing(clock); }; @@ -247,7 +241,7 @@ fun test_create_multiple_trails() { ); trail_ids.push_back(trail_id1); - ts::return_to_sender(&scenario, admin_cap1); + admin_cap1.destroy_for_testing(); clock::destroy_for_testing(clock); }; @@ -279,7 +273,7 @@ fun test_create_multiple_trails() { // Verify trails have different IDs assert!(trail_ids[0] != trail_ids[1], 0); - ts::return_to_sender(&scenario, admin_cap2); + admin_cap2.destroy_for_testing(); clock::destroy_for_testing(clock); }; From 713c494e3f28fc0ae874dbb43b4045964cc52132 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Mon, 22 Dec 2025 11:38:01 +0000 Subject: [PATCH 07/18] New test test_create_metadata_admin_role() in create_tests.move --- audit-trail-move/tests/create_tests.move | 77 ++++++++++++++++++++ audit-trail-move/tests/permission_tests.move | 9 +++ 2 files changed, 86 insertions(+) diff --git a/audit-trail-move/tests/create_tests.move b/audit-trail-move/tests/create_tests.move index e29f8d4..cb554b3 100644 --- a/audit-trail-move/tests/create_tests.move +++ b/audit-trail-move/tests/create_tests.move @@ -279,3 +279,80 @@ fun test_create_multiple_trails() { ts::end(scenario); } + +/// Test creating a MetaDataAdmin role with metadata_admin_permissions. +/// +/// This test verifies that: +/// 1. A creator can create an AuditTrail and receive an admin capability +/// 2. The admin capability can be transferred to another user +/// 3. The user can use the capability to create a new MetaDataAdmin role +/// 4. The new role has the correct permissions (meta_data_update and meta_data_delete) +#[test] +fun test_create_metadata_admin_role() { + let creator = @0xA; + let user = @0xB; + let mut scenario = ts::begin(creator); + + // Creator creates the audit trail + { + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(1000); + + let locking_config = locking::new(std::option::none(), std::option::some(0)); + let trail_metadata = main::new_trail_metadata( + std::option::some(string::utf8(b"Test Trail for MetaDataAdmin")), + std::option::some(string::utf8(b"Testing metadata admin role creation")), + ); + + let (admin_cap, trail_id) = main::create( + std::option::none(), + std::option::none(), + locking_config, + trail_metadata, + std::option::some(string::utf8(b"Initial metadata")), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify admin capability was created + assert!(admin_cap.role() == initial_admin_role_name(), 0); + assert!(admin_cap.trail_id() == trail_id, 1); + + // Transfer the admin capability to the user + transfer::public_transfer(admin_cap, user); + + clock::destroy_for_testing(clock); + }; + + // User receives the capability and creates the MetaDataAdmin role + ts::next_tx(&mut scenario, user); + { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + // Create the MetaDataAdmin role using the admin capability + let metadata_admin_role_name = string::utf8(b"MetaDataAdmin"); + let metadata_admin_perms = audit_trail::permission::metadata_admin_permissions(); + + trail.create_role( + &admin_cap, + metadata_admin_role_name, + metadata_admin_perms, + ts::ctx(&mut scenario), + ); + + // Verify the role was created by fetching its permissions + let role_perms = trail.get_role_permissions(&string::utf8(b"MetaDataAdmin")); + + // Verify the role has the correct permissions + assert!(audit_trail::permission::has_permission(role_perms, &audit_trail::permission::meta_data_update()), 2); + assert!(audit_trail::permission::has_permission(role_perms, &audit_trail::permission::meta_data_delete()), 3); + assert!(iota::vec_set::size(role_perms) == 2, 4); + + // Clean up + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + }; + + ts::end(scenario); +} diff --git a/audit-trail-move/tests/permission_tests.move b/audit-trail-move/tests/permission_tests.move index ac13674..2653c1e 100644 --- a/audit-trail-move/tests/permission_tests.move +++ b/audit-trail-move/tests/permission_tests.move @@ -89,6 +89,15 @@ fun test_from_vec_multiple_permission() { assert!(!permission::has_permission(&set, &permission::record_correct()), 0); } +#[test] +fun test_metadata_admin_permissions() { + let perms = permission::metadata_admin_permissions(); + + assert!(permission::has_permission(&perms, &permission::meta_data_update()), 0); + assert!(permission::has_permission(&perms, &permission::meta_data_delete()), 0); + assert!(iota::vec_set::size(&perms) == 2, 0); +} + #[test] #[expected_failure(abort_code = vec_set::EKeyAlreadyExists)] fun test_from_vec_duplicate_permission() { From 6cb397b2ce059b046d49e9bf828ae59bfb8aeb3b Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Mon, 22 Dec 2025 11:39:40 +0000 Subject: [PATCH 08/18] Fix dprint issues --- audit-trail-rs/README.md | 2 +- bindings/wasm/audit_trails_wasm/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/audit-trail-rs/README.md b/audit-trail-rs/README.md index 02dd617..71a444e 100644 --- a/audit-trail-rs/README.md +++ b/audit-trail-rs/README.md @@ -1 +1 @@ -# IOTA Audit Trails \ No newline at end of file +# IOTA Audit Trails diff --git a/bindings/wasm/audit_trails_wasm/README.md b/bindings/wasm/audit_trails_wasm/README.md index 726f73a..dcd4113 100644 --- a/bindings/wasm/audit_trails_wasm/README.md +++ b/bindings/wasm/audit_trails_wasm/README.md @@ -1 +1 @@ -# IOTA Audit Trails WASM Library \ No newline at end of file +# IOTA Audit Trails WASM Library From bc512d842caf5bdc140b8a2fdc9a4d1b21e4761d Mon Sep 17 00:00:00 2001 From: Yasir Date: Mon, 22 Dec 2025 15:36:44 +0300 Subject: [PATCH 09/18] refactor: Rename audit_trails module to main and update locking configuration structure --- audit-trails-move/sources/audit_trails.move | 6 +- audit-trails-move/sources/locking.move | 121 ++++++++++++++------ 2 files changed, 92 insertions(+), 35 deletions(-) diff --git a/audit-trails-move/sources/audit_trails.move b/audit-trails-move/sources/audit_trails.move index 89aebd3..0fc8560 100644 --- a/audit-trails-move/sources/audit_trails.move +++ b/audit-trails-move/sources/audit_trails.move @@ -7,7 +7,7 @@ /// references its predecessor, ensuring verifiable continuity and integrity. /// /// Records are addressed by trail_id + sequence_number -module audit_trails::audit_trails; +module audit_trails::main; use audit_trails::capabilities::{Self, Capability}; use audit_trails::locking::{Self, LockingConfig}; @@ -47,7 +47,7 @@ public struct AuditTrail has key, store { /// Deletion locking rules locking_config: LockingConfig, /// Role name -> set of permissions (TODO: implement) - permissions: VecMap>, + roles: VecMap>, /// Set at creation, cannot be changed immutable_metadata: TrailImmutableMetadata, /// Can be updated by holders of MetadataUpdate permission @@ -140,7 +140,7 @@ public fun create( record_count, records, locking_config, - permissions: vec_map::empty(), + roles: vec_map::empty(), immutable_metadata: trail_metadata, updatable_metadata, issued_capabilities: iota::vec_set::empty(), diff --git a/audit-trails-move/sources/locking.move b/audit-trails-move/sources/locking.move index c20ef50..e59fe3d 100644 --- a/audit-trails-move/sources/locking.move +++ b/audit-trails-move/sources/locking.move @@ -2,76 +2,114 @@ // SPDX-License-Identifier: Apache-2.0 /// Locking configuration for audit trail records -/// -/// Controls when records can be deleted based on time window (records locked for N seconds) -/// or count window (last N records always locked). module audit_trails::locking; -/// Controls when records can be deleted (time OR count based) -public struct LockingConfig has copy, drop, store { +/// Defines a locking window (time OR count based) +public struct LockingWindow has copy, drop, store { /// Records locked for N seconds after creation time_window_seconds: Option, /// Last N records are always locked count_window: Option, } -// ===== Constructors ===== +/// Top-level locking configuration for the audit trail +public struct LockingConfig has copy, drop, store { + /// Locking rules for record deletion + delete_record_lock: LockingWindow, +} + +// ===== LockingWindow Constructors ===== -/// Create a new locking configuration +/// Create a new locking window /// /// - `time_window_seconds`: Records are locked for N seconds after creation (None = no time lock) /// - `count_window`: Last N records are always locked (None = no count lock) -public fun new(time_window_seconds: Option, count_window: Option): LockingConfig { - LockingConfig { time_window_seconds, count_window } +public fun new_window(time_window_seconds: Option, count_window: Option): LockingWindow { + LockingWindow { time_window_seconds, count_window } +} + +/// Create a locking window with no restrictions +public fun window_none(): LockingWindow { + LockingWindow { + time_window_seconds: option::none(), + count_window: option::none(), + } +} + +/// Create a time-based locking window +public fun window_time_based(seconds: u64): LockingWindow { + LockingWindow { + time_window_seconds: option::some(seconds), + count_window: option::none(), + } +} + +/// Create a count-based locking window +public fun window_count_based(count: u64): LockingWindow { + LockingWindow { + time_window_seconds: option::none(), + count_window: option::some(count), + } +} + +// ===== LockingConfig Constructors ===== + +/// Create a new locking configuration +public fun new(delete_record_lock: LockingWindow): LockingConfig { + LockingConfig { delete_record_lock } } /// Create a locking config with no restrictions public fun none(): LockingConfig { LockingConfig { - time_window_seconds: option::none(), - count_window: option::none(), + delete_record_lock: window_none(), } } -/// Create a time-based locking config +/// Create a locking config with time-based record deletion lock public fun time_based(seconds: u64): LockingConfig { LockingConfig { - time_window_seconds: option::some(seconds), - count_window: option::none(), + delete_record_lock: window_time_based(seconds), } } -/// Create a count-based locking config +/// Create a locking config with count-based record deletion lock public fun count_based(count: u64): LockingConfig { LockingConfig { - time_window_seconds: option::none(), - count_window: option::some(count), + delete_record_lock: window_count_based(count), } } -// ===== Getters ===== +// ===== LockingWindow Getters ===== /// Get the time window in seconds (if set) -public fun time_window_seconds(config: &LockingConfig): &Option { - &config.time_window_seconds +public fun time_window_seconds(window: &LockingWindow): &Option { + &window.time_window_seconds } /// Get the count window (if set) -public fun count_window(config: &LockingConfig): &Option { - &config.count_window +public fun count_window(window: &LockingWindow): &Option { + &window.count_window } -// ===== Locking Logic ===== +// ===== LockingConfig Getters ===== + +/// Get the record deletion locking window +public fun delete_record_lock(config: &LockingConfig): &LockingWindow { + &config.delete_record_lock +} + +// ===== Locking Logic (LockingWindow) ===== /// Check if a record is locked based on time window /// /// Returns true if the record was created within the time window -public fun is_time_locked(config: &LockingConfig, record_timestamp: u64, current_time: u64): bool { - if (config.time_window_seconds.is_none()) { +public fun is_time_locked(window: &LockingWindow, record_timestamp: u64, current_time: u64): bool { + if (window.time_window_seconds.is_none()) { return false }; - let time_window_ms = (*config.time_window_seconds.borrow()) * 1000; + let time_window_ms = (*window.time_window_seconds.borrow()) * 1000; let record_age = current_time - record_timestamp; record_age < time_window_ms } @@ -79,18 +117,32 @@ public fun is_time_locked(config: &LockingConfig, record_timestamp: u64, current /// Check if a record is locked based on count window /// /// Returns true if the record is among the last N records -public fun is_count_locked(config: &LockingConfig, sequence_number: u64, total_records: u64): bool { - if (config.count_window.is_none()) { +public fun is_count_locked(window: &LockingWindow, sequence_number: u64, total_records: u64): bool { + if (window.count_window.is_none()) { return false }; - let count_window = *config.count_window.borrow(); + let count_window = *window.count_window.borrow(); let records_after = total_records - sequence_number - 1; records_after < count_window } -/// Check if a record is locked (either by time or count) +/// Check if a record is locked by a window (either by time or count) +public fun is_window_locked( + window: &LockingWindow, + sequence_number: u64, + record_timestamp: u64, + total_records: u64, + current_time: u64, +): bool { + is_time_locked(window, record_timestamp, current_time) + || is_count_locked(window, sequence_number, total_records) +} + +// ===== Locking Logic (LockingConfig) ===== + +/// Check if a record is locked for deletion public fun is_locked( config: &LockingConfig, sequence_number: u64, @@ -98,6 +150,11 @@ public fun is_locked( total_records: u64, current_time: u64, ): bool { - is_time_locked(config, record_timestamp, current_time) - || is_count_locked(config, sequence_number, total_records) + is_window_locked( + &config.delete_record_lock, + sequence_number, + record_timestamp, + total_records, + current_time, + ) } From ed79a98d49b96fd5832da6147c9bd0b1a2d33cc0 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Mon, 22 Dec 2025 20:08:59 +0000 Subject: [PATCH 10/18] New test for the role-based access control delegation workflow --- audit-trail-move/sources/audit_trail.move | 107 ++++++----- audit-trail-move/sources/capability.move | 1 - audit-trail-move/tests/create_tests.move | 137 +++----------- audit-trail-move/tests/role_tests.move | 214 ++++++++++++++++++++++ audit-trail-move/tests/test_utils.move | 57 ++++++ 5 files changed, 363 insertions(+), 153 deletions(-) create mode 100644 audit-trail-move/tests/role_tests.move create mode 100644 audit-trail-move/tests/test_utils.move diff --git a/audit-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move index 5bdba96..8f4d48f 100644 --- a/audit-trail-move/sources/audit_trail.move +++ b/audit-trail-move/sources/audit_trail.move @@ -214,7 +214,7 @@ public fun initial_admin_role_name(): String { /// Records are added sequentially with auto-assigned sequence numbers. /// /// TODO: Add capability parameter and permission check once implemented -public fun add_record( +public fun trail_add_record( trail: &mut AuditTrail, cap: &Capability, stored_data: D, @@ -222,7 +222,7 @@ public fun add_record( clock: &Clock, ctx: &mut TxContext, ) { - // TODO: check_permission(trail, cap, &permissions::record_add(), ctx); + assert!(trail.has_capability_permission(cap, &permission::record_add()), EPermissionDenied); let caller = ctx.sender(); let timestamp = clock::timestamp_ms(clock); @@ -368,7 +368,12 @@ public fun trail_has_record(trail: &AuditTrail, sequence_num linked_table::contains(&trail.records, sequence_number) } -// ===== Role and Capability related Functions ===== +/// Returns all records of the audit trail +public fun trail_records(trail: &AuditTrail): &LinkedTable> { + &trail.records +} + +// ===== Role related Functions ===== /// Get the permissions associated with a specific role. /// Aborts with ERoleDoesNotExist if the role does not exist. @@ -380,6 +385,58 @@ public fun trail_get_role_permissions( vec_map::get(&trail.roles, role) } +/// Create a new role consisting of a role name and associated permissions +public fun trail_create_role( + trail: &mut AuditTrail, + cap: &Capability, + role: String, + permissions: VecSet, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::roles_add()), EPermissionDenied); + vec_map::insert(&mut trail.roles, role, permissions); +} + +/// Delete an existing role +public fun trail_delete_role( + trail: &mut AuditTrail, + cap: &Capability, + role: &String, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::roles_delete()), EPermissionDenied); + vec_map::remove(&mut trail.roles, role); +} + +/// Update permissions associated with an existing role +public fun trail_update_role_permissions( + trail: &mut AuditTrail, + cap: &Capability, + role: &String, + new_permissions: VecSet, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::roles_update()), EPermissionDenied); + assert!(vec_map::contains(&trail.roles, role), ERoleDoesNotExist); + vec_map::insert(&mut trail.roles, *role, new_permissions); +} + +/// Returns the roles defined in the audit trail +public fun trail_roles(trail: &AuditTrail): &VecMap> { + &trail.roles +} + +/// Indicates if the specified role exists in the audit trail +public fun trail_has_role( + trail: &AuditTrail, + role: &String, +): bool { + vec_map::contains(&trail.roles, role) +} + + +// ===== Capability related Functions ===== + /// Indicates if a provided capability has a specific permission. public fun trail_has_capability_permission( trail: &AuditTrail, @@ -389,7 +446,7 @@ public fun trail_has_capability_permission( assert!(trail.id() == cap.trail_id(), ETrailIdNotCorrect); let permissions = trail.get_role_permissions(cap.role()); vec_set::contains(permissions, permission) -} +} /// Create a new capability with a specific role public fun trail_new_capability( @@ -429,48 +486,14 @@ public fun trail_revoke_capability( // TODO: Implement revocation logic (e.g., remove from issued_capability set) } -/// Create a new role consisting of a role name and associated permissions -public fun trail_create_role( - trail: &mut AuditTrail, - cap: &Capability, - role: String, - permissions: VecSet, - _ctx: &mut TxContext, -) { - assert!(trail.has_capability_permission(cap, &permission::roles_add()), EPermissionDenied); - vec_map::insert(&mut trail.roles, role, permissions); -} - -/// Delete an existing role -public fun trail_delete_role( - trail: &mut AuditTrail, - cap: &Capability, - role: &String, - _ctx: &mut TxContext, -) { - assert!(trail.has_capability_permission(cap, &permission::roles_delete()), EPermissionDenied); - vec_map::remove(&mut trail.roles, role); -} - -/// Update permissions associated with an existing role -public fun trail_update_role_permissions( - trail: &mut AuditTrail, - cap: &Capability, - role: &String, - new_permissions: VecSet, - _ctx: &mut TxContext, -) { - assert!(trail.has_capability_permission(cap, &permission::roles_update()), EPermissionDenied); - assert!(vec_map::contains(&trail.roles, role), ERoleDoesNotExist); - vec_map::insert(&mut trail.roles, *role, new_permissions); -} - // ===== public use statements ===== public use fun trail_id as AuditTrail.id; public use fun trail_creator as AuditTrail.creator; public use fun trail_created_at as AuditTrail.created_at; +public use fun trail_add_record as AuditTrail.add_record; public use fun trail_record_count as AuditTrail.record_count; +public use fun trail_records as AuditTrail.records; public use fun trail_name as AuditTrail.name; public use fun trail_description as AuditTrail.description; public use fun trail_metadata as AuditTrail.metadata; @@ -480,11 +503,13 @@ public use fun trail_first_sequence as AuditTrail.first_sequence; public use fun trail_last_sequence as AuditTrail.last_sequence; public use fun trail_get_record as AuditTrail.get_record; public use fun trail_has_record as AuditTrail.has_record; -public use fun trail_get_role_permissions as AuditTrail.get_role_permissions; public use fun trail_has_capability_permission as AuditTrail.has_capability_permission; public use fun trail_new_capability as AuditTrail.new_capability; public use fun trail_destroy_capability as AuditTrail.destroy_capability; public use fun trail_revoke_capability as AuditTrail.revoke_capability; +public use fun trail_get_role_permissions as AuditTrail.get_role_permissions; public use fun trail_create_role as AuditTrail.create_role; public use fun trail_delete_role as AuditTrail.delete_role; public use fun trail_update_role_permissions as AuditTrail.update_role_permissions; +public use fun trail_roles as AuditTrail.roles; +public use fun trail_has_role as AuditTrail.has_role; \ No newline at end of file diff --git a/audit-trail-move/sources/capability.move b/audit-trail-move/sources/capability.move index 7cd6a81..bf53c96 100644 --- a/audit-trail-move/sources/capability.move +++ b/audit-trail-move/sources/capability.move @@ -28,7 +28,6 @@ public(package) fun new_capability( } } - // TODO: Is this needed? What is a setup capability? // // /// Create a setup capability for trail initialization diff --git a/audit-trail-move/tests/create_tests.move b/audit-trail-move/tests/create_tests.move index cb554b3..1555a89 100644 --- a/audit-trail-move/tests/create_tests.move +++ b/audit-trail-move/tests/create_tests.move @@ -4,39 +4,23 @@ module audit_trail::create_tests; use audit_trail::main::{Self, AuditTrail, initial_admin_role_name}; use audit_trail::locking::{Self}; use audit_trail::capability::{Capability}; +use audit_trail::test_utils::{setup_test_audit_trail, new_test_data, initial_time_for_testing, TestData}; use iota::test_scenario::{Self as ts}; use iota::clock::{Self}; use std::string::{Self}; -/// Test data type for audit trail records -public struct TestData has store, copy, drop { - value: u64, - message: vector, -} - #[test] fun test_create_without_initial_record() { let user = @0xA; let mut scenario = ts::begin(user); { - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); - clock.set_for_testing(1000); - let locking_config = locking::new(std::option::none(), std::option::some(0)); - let trail_metadata = main::new_trail_metadata( - std::option::some(string::utf8(b"Test Trail")), - std::option::some(string::utf8(b"A test audit trail")), - ); - let (admin_cap, trail_id) = main::create( - std::option::none(), - std::option::none(), + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, locking_config, - trail_metadata, - std::option::some(string::utf8(b"Updatable metadata")), - &clock, - ts::ctx(&mut scenario), + std::option::none() ); // Verify capability was created @@ -44,7 +28,6 @@ fun test_create_without_initial_record() { assert!(admin_cap.trail_id() == trail_id, 1); // Clean up - clock::destroy_for_testing(clock); admin_cap.destroy_for_testing(); }; @@ -54,7 +37,7 @@ fun test_create_without_initial_record() { // Verify trail was created correctly assert!(trail.trail_creator() == user, 2); - assert!(trail.trail_created_at() == 1000, 3); + assert!(trail.trail_created_at() == initial_time_for_testing(), 3); assert!(trail.trail_record_count() == 0, 4); ts::return_shared(trail); @@ -69,28 +52,13 @@ fun test_create_with_initial_record() { let mut scenario = ts::begin(user); { - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); - clock.set_for_testing(2000); - let locking_config = locking::new(std::option::some(86400), std::option::none()); // 1 day in seconds - let trail_metadata = main::new_trail_metadata( - std::option::some(string::utf8(b"Test Trail with Record")), - std::option::some(string::utf8(b"A test audit trail with initial record")), - ); - - let initial_data = TestData { - value: 42, - message: b"Hello, World!", - }; + let initial_data = new_test_data(42, b"Hello, World!"); - let (admin_cap, trail_id) = main::create( - std::option::some(initial_data), - std::option::some(string::utf8(b"Initial record metadata")), + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, locking_config, - trail_metadata, - std::option::some(string::utf8(b"Updatable metadata")), - &clock, - ts::ctx(&mut scenario), + std::option::some(initial_data) ); // Verify capability @@ -98,7 +66,6 @@ fun test_create_with_initial_record() { assert!(admin_cap.trail_id() == trail_id, 1); // Clean up - clock::destroy_for_testing(clock); admin_cap.destroy_for_testing(); }; @@ -108,7 +75,7 @@ fun test_create_with_initial_record() { // Verify trail with initial record assert!(trail.trail_creator() == user, 2); - assert!(trail.trail_created_at() == 2000, 3); + assert!(trail.trail_created_at() == initial_time_for_testing(), 3); assert!(trail.trail_record_count() == 1, 4); // Verify the initial record exists @@ -173,29 +140,16 @@ fun test_create_with_locking_enabled() { let user = @0xD; let mut scenario = ts::begin(user); - { - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); - clock.set_for_testing(4000); - + { let locking_config = locking::new(std::option::some(604800), std::option::none()); // 7 days in seconds - let trail_metadata = main::new_trail_metadata( - std::option::some(string::utf8(b"Locked Trail")), - std::option::none(), - ); - - let (admin_cap, _trail_id) = main::create( - std::option::none(), - std::option::none(), + let (admin_cap, _trail_id) = setup_test_audit_trail( + &mut scenario, locking_config, - trail_metadata, - std::option::none(), - &clock, - ts::ctx(&mut scenario), + std::option::none() ); // Clean up admin_cap.destroy_for_testing(); - clock::destroy_for_testing(clock); }; ts::next_tx(&mut scenario, user); @@ -220,52 +174,27 @@ fun test_create_multiple_trails() { let mut trail_ids = vector::empty(); // Create first trail - { - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); - clock.set_for_testing(5000); - + { let locking_config = locking::new(std::option::none(), std::option::some(0)); - let trail_metadata = main::new_trail_metadata( - std::option::some(string::utf8(b"Trail 1")), - std::option::none(), - ); - - let (admin_cap1, trail_id1) = main::create( - std::option::none(), - std::option::none(), + let (admin_cap1, trail_id1) = setup_test_audit_trail( + &mut scenario, locking_config, - trail_metadata, - std::option::none(), - &clock, - ts::ctx(&mut scenario), + std::option::none() ); trail_ids.push_back(trail_id1); admin_cap1.destroy_for_testing(); - clock::destroy_for_testing(clock); }; ts::next_tx(&mut scenario, user); // Create second trail { - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); - clock.set_for_testing(6000); - let locking_config = locking::new(std::option::none(), std::option::some(0)); - let trail_metadata = main::new_trail_metadata( - std::option::some(string::utf8(b"Trail 2")), - std::option::none(), - ); - - let (admin_cap2, trail_id2) = main::create( - std::option::none(), - std::option::none(), + let (admin_cap2, trail_id2) = setup_test_audit_trail( + &mut scenario, locking_config, - trail_metadata, - std::option::none(), - &clock, - ts::ctx(&mut scenario), + std::option::none() ); trail_ids.push_back(trail_id2); @@ -274,7 +203,6 @@ fun test_create_multiple_trails() { assert!(trail_ids[0] != trail_ids[1], 0); admin_cap2.destroy_for_testing(); - clock::destroy_for_testing(clock); }; ts::end(scenario); @@ -294,24 +222,13 @@ fun test_create_metadata_admin_role() { let mut scenario = ts::begin(creator); // Creator creates the audit trail - { - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); - clock.set_for_testing(1000); - + { let locking_config = locking::new(std::option::none(), std::option::some(0)); - let trail_metadata = main::new_trail_metadata( - std::option::some(string::utf8(b"Test Trail for MetaDataAdmin")), - std::option::some(string::utf8(b"Testing metadata admin role creation")), - ); - let (admin_cap, trail_id) = main::create( - std::option::none(), - std::option::none(), + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, locking_config, - trail_metadata, - std::option::some(string::utf8(b"Initial metadata")), - &clock, - ts::ctx(&mut scenario), + std::option::none() ); // Verify admin capability was created @@ -319,9 +236,7 @@ fun test_create_metadata_admin_role() { assert!(admin_cap.trail_id() == trail_id, 1); // Transfer the admin capability to the user - transfer::public_transfer(admin_cap, user); - - clock::destroy_for_testing(clock); + transfer::public_transfer(admin_cap, user); }; // User receives the capability and creates the MetaDataAdmin role diff --git a/audit-trail-move/tests/role_tests.move b/audit-trail-move/tests/role_tests.move new file mode 100644 index 0000000..70d2174 --- /dev/null +++ b/audit-trail-move/tests/role_tests.move @@ -0,0 +1,214 @@ +#[test_only] +module audit_trail::role_tests; + +use audit_trail::permission::{Self}; +use audit_trail::locking::{Self}; +use audit_trail::main::{Self, AuditTrail, initial_admin_role_name}; +use audit_trail::test_utils::{Self, TestData, setup_test_audit_trail}; +use iota::test_scenario::{Self as ts}; +use iota::clock::{Self}; +use std::string::{Self}; +use audit_trail::capability::Capability; + +/// Test comprehensive role-based access control delegation workflow. +/// +/// This test validates the complete permission delegation chain: +/// 1. An admin user creates an audit trail with full admin permissions +/// 2. Admin creates two specialized roles: RoleAdmin (for role management) and CapAdmin (for capability management) +/// 3. Admin delegates these roles to different users by issuing capabilities +/// 4. RoleAdmin user leverages their permissions to create a RecordAdmin role +/// 5. CapAdmin user leverages their permissions to issue a RecordAdmin capability +/// 6. RecordAdmin user uses their capability to add a record to the audit trail +/// +/// This test ensures: +/// - Role creation works correctly with specific permission sets +/// - Capability issuance and transfer functions properly +/// - Permission delegation cascade works (Admin -> RoleAdmin -> RecordAdmin) +/// - Permission delegation cascade works (Admin -> CapAdmin -> RecordAdmin capability) +#[test] +fun test_role_based_permission_delegation() { + let admin_user = @0xAD; + let role_admin_user = @0xB0B; + let cap_admin_user = @0xCAB; + let record_admin_user = @0xDED; + + let mut scenario = ts::begin(admin_user); + + // Step 1: admin_user creates the audit trail + let trail_id = { + let locking_config = locking::new(std::option::none(), std::option::some(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + // Verify admin capability was created with correct role and trail reference + assert!(admin_cap.role() == initial_admin_role_name(), 0); + assert!(admin_cap.trail_id() == trail_id, 1); + + // Transfer the admin capability to the user + transfer::public_transfer(admin_cap, admin_user); + + trail_id + }; + + // Step 2: Admin creates RoleAdmin and CapAdmin roles + ts::next_tx(&mut scenario, admin_user); + { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + // Verify initial state - should only have the initial admin role + assert!(trail.roles().size() == 1, 2); + + // Create RoleAdmin role + let role_admin_perms = permission::role_admin_permissions(); + trail.create_role( + &admin_cap, + string::utf8(b"RoleAdmin"), + role_admin_perms, + ts::ctx(&mut scenario), + ); + + // Create CapAdmin role + let cap_admin_perms = permission::cap_admin_permissions(); + trail.create_role( + &admin_cap, + string::utf8(b"CapAdmin"), + cap_admin_perms, + ts::ctx(&mut scenario), + ); + + // Verify both roles were created + assert!(trail.roles().size() == 3, 3); // Initial admin + RoleAdmin + CapAdmin + assert!(trail.has_role(&string::utf8(b"RoleAdmin")), 4); + assert!(trail.has_role(&string::utf8(b"CapAdmin")), 5); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + }; + + // Step 3: Admin creates capability for RoleAdmin and CapAdmin and transfers to the respective users + ts::next_tx(&mut scenario, admin_user); + { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let role_admin_cap = trail.new_capability( + &admin_cap, + &string::utf8(b"RoleAdmin"), + ts::ctx(&mut scenario), + ); + + // Verify the capability was created with correct role and trail ID + assert!(role_admin_cap.role() == string::utf8(b"RoleAdmin"), 6); + assert!(role_admin_cap.trail_id() == trail_id, 7); + + iota::transfer::public_transfer(role_admin_cap, role_admin_user); + + let cap_admin_cap = trail.new_capability( + &admin_cap, + &string::utf8(b"CapAdmin"), + ts::ctx(&mut scenario), + ); + + // Verify the capability was created with correct role and trail ID + assert!(cap_admin_cap.role() == string::utf8(b"CapAdmin"), 8); + assert!(cap_admin_cap.trail_id() == trail_id, 9); + + iota::transfer::public_transfer(cap_admin_cap, cap_admin_user); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + }; + + + // Step 5: RoleAdmin creates RecordAdmin role (demonstrating delegated role management) + ts::next_tx(&mut scenario, role_admin_user); + { + let mut trail = ts::take_shared>(&scenario); + let role_admin_cap = ts::take_from_sender(&scenario); + + // Verify RoleAdmin has the correct role + assert!(role_admin_cap.role() == string::utf8(b"RoleAdmin"), 10); + + let record_admin_perms = permission::record_admin_permissions(); + trail.create_role( + &role_admin_cap, + string::utf8(b"RecordAdmin"), + record_admin_perms, + ts::ctx(&mut scenario), + ); + + // Verify RecordAdmin role was created successfully + assert!(trail.roles().size() == 4, 11); // Initial admin + RoleAdmin + CapAdmin + RecordAdmin + assert!(trail.has_role(&string::utf8(b"RecordAdmin")), 12); + + ts::return_to_sender(&scenario, role_admin_cap); + ts::return_shared(trail); + }; + + // Step 6: CapAdmin creates capability for RecordAdmin and transfers to record_admin_user + ts::next_tx(&mut scenario, cap_admin_user); + { + let mut trail = ts::take_shared>(&scenario); + let cap_admin_cap = ts::take_from_sender(&scenario); + + // Verify CapAdmin has the correct role + assert!(cap_admin_cap.role() == string::utf8(b"CapAdmin"), 13); + + let record_admin_cap = trail.new_capability( + &cap_admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + + // Verify the capability was created with correct role and trail ID + assert!(record_admin_cap.role() == string::utf8(b"RecordAdmin"), 14); + assert!(record_admin_cap.trail_id() == trail_id, 15); + + iota::transfer::public_transfer(record_admin_cap, record_admin_user); + + ts::return_to_sender(&scenario, cap_admin_cap); + ts::return_shared(trail); + }; + + // Step 7: RecordAdmin adds a new record to the audit trail (demonstrating delegated record management) + ts::next_tx(&mut scenario, record_admin_user); + { + let mut trail = ts::take_shared>(&scenario); + let record_admin_cap = ts::take_from_sender(&scenario); + + // Verify RecordAdmin has the correct role + assert!(record_admin_cap.role() == string::utf8(b"RecordAdmin"), 16); + + // Verify initial record count + let initial_record_count = trail.records().length(); + + let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(test_utils::initial_time_for_testing() + 1000); + + let test_data = test_utils::new_test_data(42, b"Test record added by RecordAdmin"); + + trail.add_record( + &record_admin_cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify the record was added successfully + assert!(trail.records().length() == initial_record_count + 1, 17); + + clock::destroy_for_testing(clock); + ts::return_to_sender(&scenario, record_admin_cap); + ts::return_shared(trail); + }; + + // Cleanup + ts::next_tx(&mut scenario, admin_user); + ts::end(scenario); +} \ No newline at end of file diff --git a/audit-trail-move/tests/test_utils.move b/audit-trail-move/tests/test_utils.move new file mode 100644 index 0000000..640b219 --- /dev/null +++ b/audit-trail-move/tests/test_utils.move @@ -0,0 +1,57 @@ + +#[test_only] +module audit_trail::test_utils; + +use audit_trail::locking::{Self}; +use audit_trail::capability::{Capability}; +use iota::clock::{Self}; +use iota::test_scenario::{Self as ts, Scenario}; +use audit_trail::main::{Self}; +use std::string::{Self}; + +const INITIAL_TIME_FOR_TESTING: u64 = 1234; + +/// Test data type for audit trail records +public struct TestData has store, copy, drop { + value: u64, + message: vector, +} + +public(package) fun new_test_data(value: u64, message: vector): TestData { + TestData { + value, + message, + } +} + +public(package) fun initial_time_for_testing(): u64 { + INITIAL_TIME_FOR_TESTING +} + +/// Setup a test audit trail with optional initial data +public(package) fun setup_test_audit_trail(scenario: &mut Scenario, locking_config: locking::LockingConfig, initial_data: Option): (Capability, iota::object::ID) { + let (admin_cap, trail_id) = { + let mut clock = clock::create_for_testing(ts::ctx(scenario)); + clock.set_for_testing(INITIAL_TIME_FOR_TESTING); + + let trail_metadata = main::new_trail_metadata( + std::option::some(string::utf8(b"Setup Test Trail")), + std::option::none(), + ); + + let (admin_cap, trail_id) = main::create( + initial_data, + std::option::none(), + locking_config, + trail_metadata, + std::option::none(), + &clock, + ts::ctx(scenario), + ); + + clock::destroy_for_testing(clock); + (admin_cap, trail_id) + }; + + (admin_cap, trail_id) +} \ No newline at end of file From 09ae22b84f529f7a74fdb10f108c6c03df9b395e Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Mon, 22 Dec 2025 20:16:40 +0000 Subject: [PATCH 11/18] Add access control check for update_metadata() --- audit-trail-move/sources/audit_trail.move | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/audit-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move index 8f4d48f..871563d 100644 --- a/audit-trail-move/sources/audit_trail.move +++ b/audit-trail-move/sources/audit_trail.move @@ -286,15 +286,13 @@ public fun update_locking_config( // ===== Metadata ===== /// Update the trail's mutable metadata -/// -/// TODO: Add capability parameter and permission check once implemented public fun update_metadata( trail: &mut AuditTrail, cap: &Capability, new_metadata: Option, _ctx: &mut TxContext, ) { - // TODO: check_permission(trail, cap, &permissions::metadata_update(), ctx); + assert!(trail.has_capability_permission(cap, &permission::meta_data_update()), EPermissionDenied); trail.updatable_metadata = new_metadata; } From 75c84879f28f709265f1562541a8c4d9a266c43a Mon Sep 17 00:00:00 2001 From: Christof Gerritsma Date: Tue, 23 Dec 2025 11:58:49 +0100 Subject: [PATCH 12/18] Update audit-trail-move/sources/permission.move Rename MetaDataUpdate to MetadataUpdate Co-authored-by: Yasir --- audit-trail-move/sources/permission.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audit-trail-move/sources/permission.move b/audit-trail-move/sources/permission.move index 71d34c2..26fed32 100644 --- a/audit-trail-move/sources/permission.move +++ b/audit-trail-move/sources/permission.move @@ -44,7 +44,7 @@ public enum Permission has copy, drop, store { // --- Meta Data related - Proposed role: `MetaDataAdmin` --- /// Update the updatable metadata field - MetaDataUpdate, + MetadataUpdate, /// Delete the updatable metadata field MetaDataDelete, } From 60f5562f5287581e9a35966ae27a25f3c737589c Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Tue, 23 Dec 2025 11:43:25 +0000 Subject: [PATCH 13/18] Implementation for the issued_capabilities whitelist management plus corresponding tests --- audit-trail-move/sources/audit_trail.move | 46 +- audit-trail-move/tests/capability_tests.move | 550 +++++++++++++++++++ audit-trail-move/tests/create_tests.move | 21 + 3 files changed, 602 insertions(+), 15 deletions(-) create mode 100644 audit-trail-move/tests/capability_tests.move diff --git a/audit-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move index 871563d..93da63b 100644 --- a/audit-trail-move/sources/audit_trail.move +++ b/audit-trail-move/sources/audit_trail.move @@ -24,10 +24,12 @@ use std::string::String; #[error] const ERecordNotFound: vector = b"Record not found at the given sequence number"; #[error] -const ERoleDoesNotExist: vector = b"The specified role does not exist in the roles map"; +const ERoleDoesNotExist: vector = b"The specified role does not exist in the `roles` map"; #[error] const EPermissionDenied: vector = b"The role associated with the provided capability does not have the required permission"; #[error] +const ECapabilityHasBeenRevoked: vector = b"The provided capability has been revoked and is no longer valid"; +#[error] const ETrailIdNotCorrect: vector = b"The trail ID associated with the provided capability does not match the audit trail"; // ===== Constants ===== @@ -61,8 +63,8 @@ public struct AuditTrail has key, store { immutable_metadata: TrailImmutableMetadata, /// Can be updated by holders of MetadataUpdate permission updatable_metadata: Option, - /// Whitelist of all issued capability IDs (TODO: implement) - issued_capability: VecSet, + /// Whitelist of all issued capability IDs + issued_capabilities: VecSet, } // ===== Events ===== @@ -172,6 +174,15 @@ public fun create( let mut roles = vec_map::empty>(); roles.insert(initial_admin_role_name(), permission::admin_permissions()); + let admin_cap = capability::new_capability( + initial_admin_role_name(), + trail_id, + ctx, + ); + let mut issued_capabilities = vec_set::empty(); + issued_capabilities.insert(admin_cap.id()); + + let trail = AuditTrail { id: trail_uid, creator, @@ -182,16 +193,10 @@ public fun create( roles, immutable_metadata: trail_metadata, updatable_metadata, - issued_capability: iota::vec_set::empty(), + issued_capabilities, }; transfer::share_object(trail); - - let admin_cap = capability::new_capability( - initial_admin_role_name(), - trail_id, - ctx, - ); event::emit(AuditTrailCreated { trail_id, @@ -432,7 +437,6 @@ public fun trail_has_role( vec_map::contains(&trail.roles, role) } - // ===== Capability related Functions ===== /// Indicates if a provided capability has a specific permission. @@ -442,11 +446,13 @@ public fun trail_has_capability_permission( permission: &Permission, ): bool { assert!(trail.id() == cap.trail_id(), ETrailIdNotCorrect); + assert!(trail.issued_capabilities.contains(&cap.id()), ECapabilityHasBeenRevoked); let permissions = trail.get_role_permissions(cap.role()); vec_set::contains(permissions, permission) } /// Create a new capability with a specific role +/// Aborts with ERoleDoesNotExist if the role does not exist. public fun trail_new_capability( trail: &mut AuditTrail, cap: &Capability, @@ -454,11 +460,14 @@ public fun trail_new_capability( ctx: &mut TxContext, ): Capability { assert!(trail.has_capability_permission(cap, &permission::capabilities_add()), EPermissionDenied); - capability::new_capability( + assert!(trail.roles.contains(role), ERoleDoesNotExist); + let new_cap = capability::new_capability( *role, trail.id(), ctx, - ) + ); + trail.issued_capabilities.insert(new_cap.id()); + new_cap } /// Destroy an existing capability @@ -471,7 +480,7 @@ public fun trail_destroy_capability( cap_to_destroy: Capability, ) { assert!(trail.id() == cap_to_destroy.trail_id(), ETrailIdNotCorrect); - // TODO: Implement revocation logic (e.g., remove from issued_capability set) + trail.issued_capabilities.remove(&cap_to_destroy.id()); cap_to_destroy.destroy(); } @@ -481,7 +490,13 @@ public fun trail_revoke_capability( cap_to_revoke: ID, ) { assert!(trail.has_capability_permission(cap, &permission::capabilities_revoke()), EPermissionDenied); - // TODO: Implement revocation logic (e.g., remove from issued_capability set) + trail.issued_capabilities.remove(&cap_to_revoke); +} + +public fun trail_issued_capabilities( + trail: &AuditTrail, +): &VecSet { + &trail.issued_capabilities } // ===== public use statements ===== @@ -505,6 +520,7 @@ public use fun trail_has_capability_permission as AuditTrail.has_capability_perm public use fun trail_new_capability as AuditTrail.new_capability; public use fun trail_destroy_capability as AuditTrail.destroy_capability; public use fun trail_revoke_capability as AuditTrail.revoke_capability; +public use fun trail_issued_capabilities as AuditTrail.issued_capabilities; public use fun trail_get_role_permissions as AuditTrail.get_role_permissions; public use fun trail_create_role as AuditTrail.create_role; public use fun trail_delete_role as AuditTrail.delete_role; diff --git a/audit-trail-move/tests/capability_tests.move b/audit-trail-move/tests/capability_tests.move new file mode 100644 index 0000000..b5e97d3 --- /dev/null +++ b/audit-trail-move/tests/capability_tests.move @@ -0,0 +1,550 @@ +#[test_only] +module audit_trail::capability_tests; + +use audit_trail::permission::{Self}; +use audit_trail::locking::{Self}; +use audit_trail::main::{AuditTrail}; +use audit_trail::test_utils::{Self, TestData, setup_test_audit_trail}; +use iota::test_scenario::{Self as ts}; +use std::string::{Self}; +use audit_trail::capability::Capability; + +/// Test that new_capability() correctly creates a capability and tracks it in issued_capabilities. +/// +/// This test validates: +/// - Capability is created with correct role and trail ID +/// - Capability ID is added to the audit trail's issued_capabilities set +/// - Multiple capabilities can be issued and all are tracked +/// - Each capability has a unique ID +#[test] +fun test_new_capability() { + let admin_user = @0xAD; + let user1 = @0xB0B; + let user2 = @0xCAB; + + let mut scenario = ts::begin(admin_user); + + // Setup: Create audit trail with admin capability + let trail_id = { + let locking_config = locking::new(std::option::none(), std::option::some(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + transfer::public_transfer(admin_cap, admin_user); + trail_id + }; + + // Create a custom role for testing + ts::next_tx(&mut scenario, admin_user); + { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let record_admin_perms = permission::record_admin_permissions(); + trail.create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + record_admin_perms, + ts::ctx(&mut scenario), + ); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + }; + + // Test: Issue first capability + ts::next_tx(&mut scenario, admin_user); + let cap1_id = { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + // Verify initial state - only admin capability should be tracked + let initial_cap_count = trail.issued_capabilities().size(); + assert!(initial_cap_count == 1, 0); // Only admin cap + + let cap1 = trail.new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + + // Verify capability was created correctly + assert!(cap1.role() == string::utf8(b"RecordAdmin"), 1); + assert!(cap1.trail_id() == trail_id, 2); + + let cap1_id = object::id(&cap1); + + // Verify capability ID is tracked in issued_capabilities + assert!(trail.issued_capabilities().size() == initial_cap_count + 1, 3); + assert!(trail.issued_capabilities().contains(&cap1_id), 4); + + transfer::public_transfer(cap1, user1); + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + + cap1_id + }; + + // Test: Issue second capability + ts::next_tx(&mut scenario, admin_user); + let _cap2_id = { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let previous_cap_count = trail.issued_capabilities().size(); + + let cap2 = trail.new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + + let cap2_id = object::id(&cap2); + + // Verify both capabilities are tracked + assert!(trail.issued_capabilities().size() == previous_cap_count + 1, 5); + assert!(trail.issued_capabilities().contains(&cap1_id), 6); + assert!(trail.issued_capabilities().contains(&cap2_id), 7); + + // Verify capabilities have unique IDs + assert!(cap1_id != cap2_id, 8); + + transfer::public_transfer(cap2, user2); + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + + cap2_id + }; + + ts::end(scenario); +} + +/// Test that revoke_capability() correctly revokes a capability and removes it from issued_capabilities. +/// +/// This test validates: +/// - Capability can be revoked by an authorized user +/// - Revoked capability ID is removed from issued_capabilities set +/// - Revoking one capability doesn't affect other capabilities +/// - Revoked capability object is properly destroyed +#[test] +fun test_revoke_capability() { + let admin_user = @0xAD; + let user1 = @0xB0B; + let user2 = @0xCAB; + + let mut scenario = ts::begin(admin_user); + + // Setup: Create audit trail with admin capability + let _trail_id = { + let locking_config = locking::new(std::option::none(), std::option::some(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + transfer::public_transfer(admin_cap, admin_user); + trail_id + }; + + // Create a custom role for testing + ts::next_tx(&mut scenario, admin_user); + { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let record_admin_perms = permission::record_admin_permissions(); + trail.create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + record_admin_perms, + ts::ctx(&mut scenario), + ); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + }; + + // Issue two capabilities + ts::next_tx(&mut scenario, admin_user); + let (cap1_id, cap2_id) = { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let cap1 = trail.new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + let cap1_id = object::id(&cap1); + transfer::public_transfer(cap1, user1); + + let cap2 = trail.new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + let cap2_id = object::id(&cap2); + transfer::public_transfer(cap2, user2); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + + (cap1_id, cap2_id) + }; + + // Test: Revoke first capability + ts::next_tx(&mut scenario, user1); + { + let admin_cap = ts::take_from_address(&scenario, admin_user); + let mut trail = ts::take_shared>(&scenario); + let cap1 = ts::take_from_sender(&scenario); + + // Verify both capabilities are tracked before revocation + let cap_count_before = trail.issued_capabilities().size(); + assert!(trail.issued_capabilities().contains(&cap1_id), 0); + assert!(trail.issued_capabilities().contains(&cap2_id), 1); + + // Revoke the capability + trail.revoke_capability( + &admin_cap, + cap1.id() + ); + + // Verify capability was removed from tracking + assert!(trail.issued_capabilities().size() == cap_count_before - 1, 2); + assert!(!trail.issued_capabilities().contains(&cap1_id), 3); + + // Verify other capability is still tracked + assert!(trail.issued_capabilities().contains(&cap2_id), 4); + + ts::return_to_address(admin_user, admin_cap); + ts::return_to_sender(&scenario, cap1); + ts::return_shared(trail); + }; + + // Verify cap1 is still available to user1 -it has been revoked, not destroyed + ts::next_tx(&mut scenario, user1); + { + // This should not find cap1 since it was revoked + assert!(ts::has_most_recent_for_sender(&scenario), 5); + }; + + // Test: Revoke second capability + ts::next_tx(&mut scenario, user2); + { + let admin_cap = ts::take_from_address(&scenario, admin_user); + let mut trail = ts::take_shared>(&scenario); + let cap2 = ts::take_from_sender(&scenario); + + let cap_count_before = trail.issued_capabilities().size(); + + trail.revoke_capability( + &admin_cap, + cap2.id() + ); + + // Verify capability was removed from tracking + assert!(trail.issued_capabilities().size() == cap_count_before - 1, 6); + assert!(!trail.issued_capabilities().contains(&cap2_id), 7); + + ts::return_to_address(admin_user, admin_cap); + ts::return_to_sender(&scenario, cap2); + ts::return_shared(trail); + }; + + ts::end(scenario); +} + +/// Test that destroy_capability() correctly destroys a capability and removes it from issued_capabilities. +/// +/// This test validates: +/// - Capability owner can destroy their own capability +/// - Destroyed capability ID is removed from issued_capabilities set +/// - Destroying one capability doesn't affect other capabilities +/// - Capability object is properly destroyed and cannot be used again +#[test] +fun test_destroy_capability() { + let admin_user = @0xAD; + let user1 = @0xB0B; + let user2 = @0xCAB; + + let mut scenario = ts::begin(admin_user); + + // Setup: Create audit trail with admin capability + let trail_id = { + let locking_config = locking::new(std::option::none(), std::option::some(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + transfer::public_transfer(admin_cap, admin_user); + trail_id + }; + + // Create a custom role for testing + ts::next_tx(&mut scenario, admin_user); + { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let record_admin_perms = permission::record_admin_permissions(); + trail.create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + record_admin_perms, + ts::ctx(&mut scenario), + ); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + }; + + // Issue two capabilities + ts::next_tx(&mut scenario, admin_user); + let (cap1_id, cap2_id) = { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let cap1 = trail.new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + let cap1_id = object::id(&cap1); + transfer::public_transfer(cap1, user1); + + let cap2 = trail.new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + let cap2_id = object::id(&cap2); + transfer::public_transfer(cap2, user2); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + + (cap1_id, cap2_id) + }; + + // Test: User1 destroys their own capability + ts::next_tx(&mut scenario, user1); + { + let mut trail = ts::take_shared>(&scenario); + let cap1 = ts::take_from_sender(&scenario); + + // Verify both capabilities are tracked before destruction + let cap_count_before = trail.issued_capabilities().size(); + assert!(trail.issued_capabilities().contains(&cap1_id), 0); + assert!(trail.issued_capabilities().contains(&cap2_id), 1); + + // Destroy the capability + trail.destroy_capability(cap1); + + // Verify capability was removed from tracking + assert!(trail.issued_capabilities().size() == cap_count_before - 1, 2); + assert!(!trail.issued_capabilities().contains(&cap1_id), 3); + + // Verify other capability is still tracked + assert!(trail.issued_capabilities().contains(&cap2_id), 4); + + ts::return_shared(trail); + }; + + // Verify cap1 is no longer available to user1 + ts::next_tx(&mut scenario, user1); + { + // This should not find cap1 since it was destroyed + assert!(!ts::has_most_recent_for_sender(&scenario), 5); + }; + + // Test: User2 destroys their own capability + ts::next_tx(&mut scenario, user2); + { + let mut trail = ts::take_shared>(&scenario); + let cap2 = ts::take_from_sender(&scenario); + + let cap_count_before = trail.issued_capabilities().size(); + + trail.destroy_capability(cap2); + + // Verify capability was removed from tracking + assert!(trail.issued_capabilities().size() == cap_count_before - 1, 6); + assert!(!trail.issued_capabilities().contains(&cap2_id), 7); + + ts::return_shared(trail); + }; + + // Verify only admin capability remains + ts::next_tx(&mut scenario, admin_user); + { + let trail = ts::take_shared>(&scenario); + + // Only the initial admin capability should remain + assert!(trail.issued_capabilities().size() == 1, 8); + + ts::return_shared(trail); + }; + + ts::end(scenario); +} + +/// Test capability lifecycle: creation, usage, and destruction in a complete workflow. +/// +/// This test validates: +/// - Multiple capabilities can be created for different roles +/// - Capabilities can be used to perform authorized actions +/// - Capabilities can be revoked or destroyed +/// - issued_capabilities tracking remains accurate throughout the lifecycle +#[test] +fun test_capability_lifecycle() { + let admin_user = @0xAD; + let record_admin_user = @0xB0B; + let role_admin_user = @0xCAB; + + let mut scenario = ts::begin(admin_user); + + // Setup: Create audit trail + let trail_id = { + let locking_config = locking::new(std::option::none(), std::option::some(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + transfer::public_transfer(admin_cap, admin_user); + trail_id + }; + + // Create roles + ts::next_tx(&mut scenario, admin_user); + { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + // Initially only admin cap should be tracked + assert!(trail.issued_capabilities().size() == 1, 0); + + trail.create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + ts::ctx(&mut scenario), + ); + + trail.create_role( + &admin_cap, + string::utf8(b"RoleAdmin"), + permission::role_admin_permissions(), + ts::ctx(&mut scenario), + ); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + }; + + // Issue capabilities + ts::next_tx(&mut scenario, admin_user); + let (record_cap_id, role_cap_id) = { + let admin_cap = ts::take_from_sender(&scenario); + let mut trail = ts::take_shared>(&scenario); + + let record_cap = trail.new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + ts::ctx(&mut scenario), + ); + let record_cap_id = object::id(&record_cap); + transfer::public_transfer(record_cap, record_admin_user); + + let role_cap = trail.new_capability( + &admin_cap, + &string::utf8(b"RoleAdmin"), + ts::ctx(&mut scenario), + ); + let role_cap_id = object::id(&role_cap); + transfer::public_transfer(role_cap, role_admin_user); + + // Verify all capabilities are tracked + assert!(trail.issued_capabilities().size() == 3, 1); // admin + record + role + assert!(trail.issued_capabilities().contains(&record_cap_id), 2); + assert!(trail.issued_capabilities().contains(&role_cap_id), 3); + + ts::return_to_sender(&scenario, admin_cap); + ts::return_shared(trail); + + (record_cap_id, role_cap_id) + }; + + // Use RecordAdmin capability to add a record + ts::next_tx(&mut scenario, record_admin_user); + { + let mut trail = ts::take_shared>(&scenario); + let record_cap = ts::take_from_sender(&scenario); + + let mut clock = iota::clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(test_utils::initial_time_for_testing() + 1000); + + let test_data = test_utils::new_test_data(1, b"Test record"); + trail.add_record( + &record_cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + iota::clock::destroy_for_testing(clock); + ts::return_to_sender(&scenario, record_cap); + ts::return_shared(trail); + }; + + // RecordAdmin destroys their capability + ts::next_tx(&mut scenario, record_admin_user); + { + let mut trail = ts::take_shared>(&scenario); + let record_cap = ts::take_from_sender(&scenario); + + trail.destroy_capability(record_cap); + + // Verify capability was removed + assert!(trail.issued_capabilities().size() == 2, 4); // admin + role + assert!(!trail.issued_capabilities().contains(&record_cap_id), 5); + + ts::return_shared(trail); + }; + + // Admin revokes RoleAdmin capability + ts::next_tx(&mut scenario, role_admin_user); + { + let admin_cap = ts::take_from_address(&scenario, admin_user); + let mut trail = ts::take_shared>(&scenario); + let role_cap = ts::take_from_sender(&scenario); + + trail.revoke_capability( + &admin_cap, + role_cap.id(), + ); + + // Verify capability was removed + assert!(trail.issued_capabilities().size() == 1, 6); // only admin remains + assert!(!trail.issued_capabilities().contains(&role_cap_id), 7); + + ts::return_to_address(admin_user, admin_cap); + ts::return_to_sender(&scenario, role_cap); + ts::return_shared(trail); + }; + + ts::end(scenario); +} \ No newline at end of file diff --git a/audit-trail-move/tests/create_tests.move b/audit-trail-move/tests/create_tests.move index 1555a89..92db017 100644 --- a/audit-trail-move/tests/create_tests.move +++ b/audit-trail-move/tests/create_tests.move @@ -1,4 +1,5 @@ #[test_only] +/// This module contains comprehensive tests for the AuditTrail creation functionality. module audit_trail::create_tests; use audit_trail::main::{Self, AuditTrail, initial_admin_role_name}; @@ -9,6 +10,10 @@ use iota::test_scenario::{Self as ts}; use iota::clock::{Self}; use std::string::{Self}; +/// Goals of this test: +/// - Verifies creating an AuditTrail with no initial record +/// - Checks admin capability creation with correct role and trail_id +/// - Validates trail metadata (creator, creation time, record count) #[test] fun test_create_without_initial_record() { let user = @0xA; @@ -46,6 +51,10 @@ fun test_create_without_initial_record() { ts::end(scenario); } +/// Goals of this test: +/// - Tests AuditTrail creation with an initial record +/// - Verifies the trail contains exactly one record after creation +/// - Validates the initial record exists at index 0 #[test] fun test_create_with_initial_record() { let user = @0xB; @@ -87,6 +96,10 @@ fun test_create_with_initial_record() { ts::end(scenario); } +/// Goals of this test: +/// - Tests creating a trail with minimal metadata (optional fields set to none) +/// - Uses a custom clock time to verify timestamp handling +/// - Ensures the system handles minimal configuration correctly #[test] fun test_create_minimal_metadata() { let user = @0xC; @@ -135,6 +148,10 @@ fun test_create_minimal_metadata() { ts::end(scenario); } +/// Goals of this test: +/// - Verifies AuditTrail creation with locking configuration enabled +/// - Tests a 7-day time-based lock period +/// - Validates the trail is created successfully with locking constraints #[test] fun test_create_with_locking_enabled() { let user = @0xD; @@ -166,6 +183,10 @@ fun test_create_with_locking_enabled() { ts::end(scenario); } +/// Goals of this test: +/// - Tests creating multiple independent AuditTrail instances +/// - Verifies each trail receives a unique ID +/// - Ensures multiple trails can coexist without conflicts #[test] fun test_create_multiple_trails() { let user = @0xE; From 520eda092903ae60503779e0dd85448d6d010030 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Tue, 23 Dec 2025 11:47:50 +0000 Subject: [PATCH 14/18] Globally rename 'MetaData' to 'Metadata' --- audit-trail-move/sources/permission.move | 10 +++++----- audit-trail-move/tests/create_tests.move | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/audit-trail-move/sources/permission.move b/audit-trail-move/sources/permission.move index 26fed32..01aaded 100644 --- a/audit-trail-move/sources/permission.move +++ b/audit-trail-move/sources/permission.move @@ -42,11 +42,11 @@ public enum Permission has copy, drop, store { /// Revoke existing capabilities CapabilitiesRevoke, - // --- Meta Data related - Proposed role: `MetaDataAdmin` --- + // --- Meta Data related - Proposed role: `MetadataAdmin` --- /// Update the updatable metadata field MetadataUpdate, /// Delete the updatable metadata field - MetaDataDelete, + MetadataDelete, } /// Create an empty permission set @@ -124,7 +124,7 @@ public fun cap_admin_permissions(): VecSet { perms } -/// Create permissions typical used for the `MetaDataAdmin` role +/// Create permissions typical used for the `MetadataAdmin` role public fun metadata_admin_permissions(): VecSet { let mut perms = vec_set::empty(); perms.insert(meta_data_update()); @@ -191,10 +191,10 @@ public fun capabilities_revoke(): Permission { /// Returns a permission allowing to update the updatable_metadata field public fun meta_data_update(): Permission { - Permission::MetaDataUpdate + Permission::MetadataUpdate } /// Returns a permission allowing to delete the updatable_metadata field public fun meta_data_delete(): Permission { - Permission::MetaDataDelete + Permission::MetadataDelete } diff --git a/audit-trail-move/tests/create_tests.move b/audit-trail-move/tests/create_tests.move index 92db017..a12d0b8 100644 --- a/audit-trail-move/tests/create_tests.move +++ b/audit-trail-move/tests/create_tests.move @@ -229,12 +229,12 @@ fun test_create_multiple_trails() { ts::end(scenario); } -/// Test creating a MetaDataAdmin role with metadata_admin_permissions. +/// Test creating a MetadataAdmin role with metadata_admin_permissions. /// /// This test verifies that: /// 1. A creator can create an AuditTrail and receive an admin capability /// 2. The admin capability can be transferred to another user -/// 3. The user can use the capability to create a new MetaDataAdmin role +/// 3. The user can use the capability to create a new MetadataAdmin role /// 4. The new role has the correct permissions (meta_data_update and meta_data_delete) #[test] fun test_create_metadata_admin_role() { @@ -260,14 +260,14 @@ fun test_create_metadata_admin_role() { transfer::public_transfer(admin_cap, user); }; - // User receives the capability and creates the MetaDataAdmin role + // User receives the capability and creates the MetadataAdmin role ts::next_tx(&mut scenario, user); { let admin_cap = ts::take_from_sender(&scenario); let mut trail = ts::take_shared>(&scenario); - // Create the MetaDataAdmin role using the admin capability - let metadata_admin_role_name = string::utf8(b"MetaDataAdmin"); + // Create the MetadataAdmin role using the admin capability + let metadata_admin_role_name = string::utf8(b"MetadataAdmin"); let metadata_admin_perms = audit_trail::permission::metadata_admin_permissions(); trail.create_role( @@ -278,7 +278,7 @@ fun test_create_metadata_admin_role() { ); // Verify the role was created by fetching its permissions - let role_perms = trail.get_role_permissions(&string::utf8(b"MetaDataAdmin")); + let role_perms = trail.get_role_permissions(&string::utf8(b"MetadataAdmin")); // Verify the role has the correct permissions assert!(audit_trail::permission::has_permission(role_perms, &audit_trail::permission::meta_data_update()), 2); From e5a71a1f518096161e72e2b27b32f404735a9a71 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Tue, 23 Dec 2025 11:51:34 +0000 Subject: [PATCH 15/18] Rename 'create_tests' to 'create_audit_trail_tests' --- .../tests/{create_tests.move => create_audit_trail_tests.move} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename audit-trail-move/tests/{create_tests.move => create_audit_trail_tests.move} (99%) diff --git a/audit-trail-move/tests/create_tests.move b/audit-trail-move/tests/create_audit_trail_tests.move similarity index 99% rename from audit-trail-move/tests/create_tests.move rename to audit-trail-move/tests/create_audit_trail_tests.move index a12d0b8..b3bbcf4 100644 --- a/audit-trail-move/tests/create_tests.move +++ b/audit-trail-move/tests/create_audit_trail_tests.move @@ -1,6 +1,6 @@ #[test_only] /// This module contains comprehensive tests for the AuditTrail creation functionality. -module audit_trail::create_tests; +module audit_trail::create_audit_trail_tests; use audit_trail::main::{Self, AuditTrail, initial_admin_role_name}; use audit_trail::locking::{Self}; From cfb8c842efdfaba226e4c6dadcd6daf3b4c9d439 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Tue, 23 Dec 2025 12:14:01 +0000 Subject: [PATCH 16/18] Rename all Permissions variants and creator functions according to the verb-subject format --- audit-trail-move/sources/audit_trail.move | 14 +-- audit-trail-move/sources/permission.move | 114 +++++++++--------- .../tests/create_audit_trail_tests.move | 4 +- audit-trail-move/tests/permission_tests.move | 62 +++++----- 4 files changed, 97 insertions(+), 97 deletions(-) diff --git a/audit-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move index 93da63b..9964982 100644 --- a/audit-trail-move/sources/audit_trail.move +++ b/audit-trail-move/sources/audit_trail.move @@ -227,7 +227,7 @@ public fun trail_add_record( clock: &Clock, ctx: &mut TxContext, ) { - assert!(trail.has_capability_permission(cap, &permission::record_add()), EPermissionDenied); + assert!(trail.has_capability_permission(cap, &permission::add_record()), EPermissionDenied); let caller = ctx.sender(); let timestamp = clock::timestamp_ms(clock); @@ -297,7 +297,7 @@ public fun update_metadata( new_metadata: Option, _ctx: &mut TxContext, ) { - assert!(trail.has_capability_permission(cap, &permission::meta_data_update()), EPermissionDenied); + assert!(trail.has_capability_permission(cap, &permission::update_metadata()), EPermissionDenied); trail.updatable_metadata = new_metadata; } @@ -396,7 +396,7 @@ public fun trail_create_role( permissions: VecSet, _ctx: &mut TxContext, ) { - assert!(trail.has_capability_permission(cap, &permission::roles_add()), EPermissionDenied); + assert!(trail.has_capability_permission(cap, &permission::add_roles()), EPermissionDenied); vec_map::insert(&mut trail.roles, role, permissions); } @@ -407,7 +407,7 @@ public fun trail_delete_role( role: &String, _ctx: &mut TxContext, ) { - assert!(trail.has_capability_permission(cap, &permission::roles_delete()), EPermissionDenied); + assert!(trail.has_capability_permission(cap, &permission::delete_roles()), EPermissionDenied); vec_map::remove(&mut trail.roles, role); } @@ -419,7 +419,7 @@ public fun trail_update_role_permissions( new_permissions: VecSet, _ctx: &mut TxContext, ) { - assert!(trail.has_capability_permission(cap, &permission::roles_update()), EPermissionDenied); + assert!(trail.has_capability_permission(cap, &permission::update_roles()), EPermissionDenied); assert!(vec_map::contains(&trail.roles, role), ERoleDoesNotExist); vec_map::insert(&mut trail.roles, *role, new_permissions); } @@ -459,7 +459,7 @@ public fun trail_new_capability( role: &String, ctx: &mut TxContext, ): Capability { - assert!(trail.has_capability_permission(cap, &permission::capabilities_add()), EPermissionDenied); + assert!(trail.has_capability_permission(cap, &permission::add_capabilities()), EPermissionDenied); assert!(trail.roles.contains(role), ERoleDoesNotExist); let new_cap = capability::new_capability( *role, @@ -489,7 +489,7 @@ public fun trail_revoke_capability( cap: &Capability, cap_to_revoke: ID, ) { - assert!(trail.has_capability_permission(cap, &permission::capabilities_revoke()), EPermissionDenied); + assert!(trail.has_capability_permission(cap, &permission::revoke_capabilities()), EPermissionDenied); trail.issued_capabilities.remove(&cap_to_revoke); } diff --git a/audit-trail-move/sources/permission.move b/audit-trail-move/sources/permission.move index 01aaded..2b5b5ce 100644 --- a/audit-trail-move/sources/permission.move +++ b/audit-trail-move/sources/permission.move @@ -10,43 +10,43 @@ use iota::vec_set::{Self, VecSet}; public enum Permission has copy, drop, store { // --- Whole AUdit TRail related - Proposed role: `Admin` --- /// Destroy the whole Audit Trail object - AuditTrailDelete, + DeleteAuditTrail, // --- Record Management - Proposed role: `RecordAdmin` --- /// Add records to the trail - RecordAdd, + AddRecord, /// Delete records from the trail - RecordDelete, + DeleteRecord, /// Correct existing records in the trail - RecordCorrect, // TODO: Clarify if needed for MVP + CorrectRecord, // TODO: Clarify if needed for MVP // --- Locking Config - Proposed role: `LockingAdmin` --- /// Edit the delete_lock configuration for records - RecordDeleteLockConfig, + ConfigRecordDeleteLock, /// Edit the delete_lock configuration for the whole Audit Trail - TrailDeleteLockConfig, + ConfigTrailDeleteLock, // --- Role Management - Proposed role: `RoleAdmin` --- /// Add new roles with associated permissions - RolesAdd, + AddRoles, /// Update permissions associated with existing roles - RolesUpdate, + UpdateRoles, /// Delete existing roles - RolesDelete, + DeleteRoles, // --- Capability Management - Proposed role: `CapAdmin` --- /// Issue new capabilities - CapabilitiesAdd, + AddCapabilities, /// Revoke existing capabilities - CapabilitiesRevoke, + RevokeCapabilities, // --- Meta Data related - Proposed role: `MetadataAdmin` --- /// Update the updatable metadata field - MetadataUpdate, + UpdateMetadata, /// Delete the updatable metadata field - MetadataDelete, + DeleteMetadata, } /// Create an empty permission set @@ -81,120 +81,120 @@ public fun has_permission(set: &VecSet, perm: &Permission): bool { /// Create permissions typical used for the `Admin` rolepermissions public fun admin_permissions(): VecSet { let mut perms = vec_set::empty(); - perms.insert(audit_trail_delete()); - perms.insert(capabilities_add()); - perms.insert(capabilities_revoke()); - perms.insert(roles_add()); - perms.insert(roles_update()); - perms.insert(roles_delete()); + perms.insert(delete_audit_trail()); + perms.insert(add_capabilities()); + perms.insert(revoke_capabilities()); + perms.insert(add_roles()); + perms.insert(update_roles()); + perms.insert(delete_roles()); perms } /// Create permissions typical used for the `RecordAdmin` role public fun record_admin_permissions(): VecSet { let mut perms = vec_set::empty(); - perms.insert(record_add()); - perms.insert(record_delete()); - perms.insert(record_correct()); + perms.insert(add_record()); + perms.insert(delete_record()); + perms.insert(correct_record()); perms } /// Create permissions typical used for the `LockingAdmin` role public fun locking_admin_permissions(): VecSet { let mut perms = vec_set::empty(); - perms.insert(record_delete_lock_config()); - perms.insert(trail_delete_lock_config()); + perms.insert(config_record_delete_lock()); + perms.insert(config_trail_delete_lock()); perms } /// Create permissions typical used for the `RoleAdmin` role public fun role_admin_permissions(): VecSet { let mut perms = vec_set::empty(); - perms.insert(roles_add()); - perms.insert(roles_update()); - perms.insert(roles_delete()); + perms.insert(add_roles()); + perms.insert(update_roles()); + perms.insert(delete_roles()); perms } /// Create permissions typical used for the `CapAdmin` role public fun cap_admin_permissions(): VecSet { let mut perms = vec_set::empty(); - perms.insert(capabilities_add()); - perms.insert(capabilities_revoke()); + perms.insert(add_capabilities()); + perms.insert(revoke_capabilities()); perms } /// Create permissions typical used for the `MetadataAdmin` role public fun metadata_admin_permissions(): VecSet { let mut perms = vec_set::empty(); - perms.insert(meta_data_update()); - perms.insert(meta_data_delete()); + perms.insert(update_metadata()); + perms.insert(delete_metadata()); perms } // --------------------------- Constructor functions for all Permission variants --------------------------- /// Returns a permission allowing to destroy the whole Audit Trail object -public fun audit_trail_delete(): Permission { - Permission::AuditTrailDelete +public fun delete_audit_trail(): Permission { + Permission::DeleteAuditTrail } /// Returns a permission allowing to add records to the trail -public fun record_add(): Permission { - Permission::RecordAdd +public fun add_record(): Permission { + Permission::AddRecord } /// Returns a permission allowing to delete records from the trail -public fun record_delete(): Permission { - Permission::RecordDelete +public fun delete_record(): Permission { + Permission::DeleteRecord } /// Returns a permission allowing to correct existing records in the trail -public fun record_correct(): Permission { - Permission::RecordCorrect +public fun correct_record(): Permission { + Permission::CorrectRecord } /// Returns a permission allowing to edit the delete_lock configuration for records -public fun record_delete_lock_config(): Permission { - Permission::RecordDeleteLockConfig +public fun config_record_delete_lock(): Permission { + Permission::ConfigRecordDeleteLock } /// Returns a permission allowing to edit the delete_lock configuration for the whole Audit Trail -public fun trail_delete_lock_config(): Permission { - Permission::TrailDeleteLockConfig +public fun config_trail_delete_lock(): Permission { + Permission::ConfigTrailDeleteLock } /// Returns a permission allowing to add new roles with associated permissions -public fun roles_add(): Permission { - Permission::RolesAdd +public fun add_roles(): Permission { + Permission::AddRoles } /// Returns a permission allowing to update permissions associated with existing roles -public fun roles_update(): Permission { - Permission::RolesUpdate +public fun update_roles(): Permission { + Permission::UpdateRoles } /// Returns a permission allowing to delete existing roles -public fun roles_delete(): Permission { - Permission::RolesDelete +public fun delete_roles(): Permission { + Permission::DeleteRoles } /// Returns a permission allowing to issue new capabilities -public fun capabilities_add(): Permission { - Permission::CapabilitiesAdd +public fun add_capabilities(): Permission { + Permission::AddCapabilities } /// Returns a permission allowing to revoke existing capabilities -public fun capabilities_revoke(): Permission { - Permission::CapabilitiesRevoke +public fun revoke_capabilities(): Permission { + Permission::RevokeCapabilities } /// Returns a permission allowing to update the updatable_metadata field -public fun meta_data_update(): Permission { - Permission::MetadataUpdate +public fun update_metadata(): Permission { + Permission::UpdateMetadata } /// Returns a permission allowing to delete the updatable_metadata field -public fun meta_data_delete(): Permission { - Permission::MetadataDelete +public fun delete_metadata(): Permission { + Permission::DeleteMetadata } diff --git a/audit-trail-move/tests/create_audit_trail_tests.move b/audit-trail-move/tests/create_audit_trail_tests.move index b3bbcf4..0eea7c7 100644 --- a/audit-trail-move/tests/create_audit_trail_tests.move +++ b/audit-trail-move/tests/create_audit_trail_tests.move @@ -281,8 +281,8 @@ fun test_create_metadata_admin_role() { let role_perms = trail.get_role_permissions(&string::utf8(b"MetadataAdmin")); // Verify the role has the correct permissions - assert!(audit_trail::permission::has_permission(role_perms, &audit_trail::permission::meta_data_update()), 2); - assert!(audit_trail::permission::has_permission(role_perms, &audit_trail::permission::meta_data_delete()), 3); + assert!(audit_trail::permission::has_permission(role_perms, &audit_trail::permission::update_metadata()), 2); + assert!(audit_trail::permission::has_permission(role_perms, &audit_trail::permission::delete_metadata()), 3); assert!(iota::vec_set::size(role_perms) == 2, 4); // Clean up diff --git a/audit-trail-move/tests/permission_tests.move b/audit-trail-move/tests/permission_tests.move index 2653c1e..c793285 100644 --- a/audit-trail-move/tests/permission_tests.move +++ b/audit-trail-move/tests/permission_tests.move @@ -13,7 +13,7 @@ fun test_has_permission_empty_set() { #[test] fun test_has_permission_single_permission() { let mut set = permission::empty(); - let perm = permission::record_add(); + let perm = permission::add_record(); permission::add(&mut set, perm); assert!(permission::has_permission(&set, &perm), 0); @@ -22,38 +22,38 @@ fun test_has_permission_single_permission() { #[test] fun test_has_permission_not_in_set() { let mut set = permission::empty(); - permission::add(&mut set, permission::record_add()); + permission::add(&mut set, permission::add_record()); - let perm = permission::record_delete(); + let perm = permission::delete_record(); assert!(!permission::has_permission(&set, &perm), 0); } #[test] fun test_has_permission_multiple_permission() { let mut set = permission::empty(); - permission::add(&mut set, permission::record_add()); - permission::add(&mut set, permission::record_delete()); - permission::add(&mut set, permission::audit_trail_delete()); + permission::add(&mut set, permission::add_record()); + permission::add(&mut set, permission::delete_record()); + permission::add(&mut set, permission::delete_audit_trail()); - assert!(permission::has_permission(&set, &permission::record_add()), 0); - assert!(permission::has_permission(&set, &permission::record_delete()), 0); - assert!(permission::has_permission(&set, &permission::audit_trail_delete()), 0); - assert!(!permission::has_permission(&set, &permission::record_correct()), 0); + assert!(permission::has_permission(&set, &permission::add_record()), 0); + assert!(permission::has_permission(&set, &permission::delete_record()), 0); + assert!(permission::has_permission(&set, &permission::delete_audit_trail()), 0); + assert!(!permission::has_permission(&set, &permission::correct_record()), 0); } #[test] fun test_has_permission_from_vec() { let perms = vector[ - permission::record_add(), - permission::record_delete(), - permission::meta_data_update(), + permission::add_record(), + permission::delete_record(), + permission::update_metadata(), ]; let set = permission::from_vec(perms); - assert!(permission::has_permission(&set, &permission::record_add()), 0); - assert!(permission::has_permission(&set, &permission::record_delete()), 0); - assert!(permission::has_permission(&set, &permission::meta_data_update()), 0); - assert!(!permission::has_permission(&set, &permission::audit_trail_delete()), 0); + assert!(permission::has_permission(&set, &permission::add_record()), 0); + assert!(permission::has_permission(&set, &permission::delete_record()), 0); + assert!(permission::has_permission(&set, &permission::update_metadata()), 0); + assert!(!permission::has_permission(&set, &permission::delete_audit_trail()), 0); } #[test] @@ -66,35 +66,35 @@ fun test_from_vec_empty() { #[test] fun test_from_vec_single_permission() { - let perms = vector[permission::record_add()]; + let perms = vector[permission::add_record()]; let set = permission::from_vec(perms); assert!(vec_set::size(&set) == 1, 0); - assert!(permission::has_permission(&set, &permission::record_add()), 0); + assert!(permission::has_permission(&set, &permission::add_record()), 0); } #[test] fun test_from_vec_multiple_permission() { let perms = vector[ - permission::record_add(), - permission::record_delete(), - permission::audit_trail_delete(), + permission::add_record(), + permission::delete_record(), + permission::delete_audit_trail(), ]; let set = permission::from_vec(perms); assert!(vec_set::size(&set) == 3, 0); - assert!(permission::has_permission(&set, &permission::record_add()), 0); - assert!(permission::has_permission(&set, &permission::record_delete()), 0); - assert!(permission::has_permission(&set, &permission::audit_trail_delete()), 0); - assert!(!permission::has_permission(&set, &permission::record_correct()), 0); + assert!(permission::has_permission(&set, &permission::add_record()), 0); + assert!(permission::has_permission(&set, &permission::delete_record()), 0); + assert!(permission::has_permission(&set, &permission::delete_audit_trail()), 0); + assert!(!permission::has_permission(&set, &permission::correct_record()), 0); } #[test] fun test_metadata_admin_permissions() { let perms = permission::metadata_admin_permissions(); - assert!(permission::has_permission(&perms, &permission::meta_data_update()), 0); - assert!(permission::has_permission(&perms, &permission::meta_data_delete()), 0); + assert!(permission::has_permission(&perms, &permission::update_metadata()), 0); + assert!(permission::has_permission(&perms, &permission::delete_metadata()), 0); assert!(iota::vec_set::size(&perms) == 2, 0); } @@ -103,9 +103,9 @@ fun test_metadata_admin_permissions() { fun test_from_vec_duplicate_permission() { // VecSet should throw error EKeyAlreadyExists on duplicate insertions let perms = vector[ - permission::record_add(), - permission::record_delete(), - permission::record_add(), // duplicate + permission::add_record(), + permission::delete_record(), + permission::add_record(), // duplicate ]; let set = permission::from_vec(perms); // The following line should not be reached due to the expected failure From 600ae26b2d49d87327bd4ba65224a8845ca22ca2 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Tue, 23 Dec 2025 18:03:21 +0000 Subject: [PATCH 17/18] Rewrite of all LockingConfig creating function calls in Move tests --- audit-trail-move/tests/capability_tests.move | 8 ++++---- .../tests/create_audit_trail_tests.move | 14 +++++++------- audit-trail-move/tests/role_tests.move | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/audit-trail-move/tests/capability_tests.move b/audit-trail-move/tests/capability_tests.move index b5e97d3..fea0f99 100644 --- a/audit-trail-move/tests/capability_tests.move +++ b/audit-trail-move/tests/capability_tests.move @@ -26,7 +26,7 @@ fun test_new_capability() { // Setup: Create audit trail with admin capability let trail_id = { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap, trail_id) = setup_test_audit_trail( &mut scenario, @@ -140,7 +140,7 @@ fun test_revoke_capability() { // Setup: Create audit trail with admin capability let _trail_id = { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap, trail_id) = setup_test_audit_trail( &mut scenario, @@ -278,7 +278,7 @@ fun test_destroy_capability() { // Setup: Create audit trail with admin capability let trail_id = { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap, trail_id) = setup_test_audit_trail( &mut scenario, @@ -415,7 +415,7 @@ fun test_capability_lifecycle() { // Setup: Create audit trail let trail_id = { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap, trail_id) = setup_test_audit_trail( &mut scenario, diff --git a/audit-trail-move/tests/create_audit_trail_tests.move b/audit-trail-move/tests/create_audit_trail_tests.move index 0eea7c7..6de3b79 100644 --- a/audit-trail-move/tests/create_audit_trail_tests.move +++ b/audit-trail-move/tests/create_audit_trail_tests.move @@ -20,7 +20,7 @@ fun test_create_without_initial_record() { let mut scenario = ts::begin(user); { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap, trail_id) = setup_test_audit_trail( &mut scenario, @@ -61,7 +61,7 @@ fun test_create_with_initial_record() { let mut scenario = ts::begin(user); { - let locking_config = locking::new(std::option::some(86400), std::option::none()); // 1 day in seconds + let locking_config = locking::new(locking::window_time_based(86400)); // 1 day in seconds let initial_data = new_test_data(42, b"Hello, World!"); let (admin_cap, trail_id) = setup_test_audit_trail( @@ -109,7 +109,7 @@ fun test_create_minimal_metadata() { let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(3000); - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let trail_metadata = main::new_trail_metadata( std::option::none(), std::option::none(), @@ -158,7 +158,7 @@ fun test_create_with_locking_enabled() { let mut scenario = ts::begin(user); { - let locking_config = locking::new(std::option::some(604800), std::option::none()); // 7 days in seconds + let locking_config = locking::new(locking::window_time_based(604800)); // 7 days in seconds let (admin_cap, _trail_id) = setup_test_audit_trail( &mut scenario, locking_config, @@ -196,7 +196,7 @@ fun test_create_multiple_trails() { // Create first trail { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap1, trail_id1) = setup_test_audit_trail( &mut scenario, locking_config, @@ -211,7 +211,7 @@ fun test_create_multiple_trails() { // Create second trail { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap2, trail_id2) = setup_test_audit_trail( &mut scenario, locking_config, @@ -244,7 +244,7 @@ fun test_create_metadata_admin_role() { // Creator creates the audit trail { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap, trail_id) = setup_test_audit_trail( &mut scenario, diff --git a/audit-trail-move/tests/role_tests.move b/audit-trail-move/tests/role_tests.move index 70d2174..e0fc15c 100644 --- a/audit-trail-move/tests/role_tests.move +++ b/audit-trail-move/tests/role_tests.move @@ -36,7 +36,7 @@ fun test_role_based_permission_delegation() { // Step 1: admin_user creates the audit trail let trail_id = { - let locking_config = locking::new(std::option::none(), std::option::some(0)); + let locking_config = locking::new(locking::window_count_based(0)); let (admin_cap, trail_id) = setup_test_audit_trail( &mut scenario, From c029e29084e00bcffd4ab2fb510412bd29743063 Mon Sep 17 00:00:00 2001 From: chrisgitiota Date: Tue, 23 Dec 2025 18:39:24 +0000 Subject: [PATCH 18/18] Add access control check for AuditTrail.update_metadata() and `delete_record_lock` --- audit-trail-move/sources/audit_trail.move | 29 +++++++++++++------- audit-trail-move/sources/locking.move | 7 +++++ audit-trail-move/sources/permission.move | 33 ++++++++++++++--------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/audit-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move index 9964982..2ff77a6 100644 --- a/audit-trail-move/sources/audit_trail.move +++ b/audit-trail-move/sources/audit_trail.move @@ -10,7 +10,7 @@ module audit_trail::main; use audit_trail::capability::{Self, Capability}; -use audit_trail::locking::{Self, LockingConfig}; +use audit_trail::locking::{Self, LockingConfig, LockingWindow, set_delete_record_lock}; use audit_trail::permission::{Self, Permission}; use audit_trail::record::{Self, Record}; use iota::clock::{Self, Clock}; @@ -217,8 +217,6 @@ public fun initial_admin_role_name(): String { /// Add a record to the trail /// /// Records are added sequentially with auto-assigned sequence numbers. -/// -/// TODO: Add capability parameter and permission check once implemented public fun trail_add_record( trail: &mut AuditTrail, cap: &Capability, @@ -256,7 +254,7 @@ public fun trail_add_record( // ===== Locking ===== /// Check if a record is locked (cannot be deleted) -public fun is_record_locked( +public fun trail_is_record_locked( trail: &AuditTrail, sequence_number: u64, clock: &Clock, @@ -276,22 +274,29 @@ public fun is_record_locked( } /// Update the locking configuration -/// -/// TODO: Add capability parameter and permission check once implemented -public fun update_locking_config( +public fun trail_update_locking_config( trail: &mut AuditTrail, cap: &Capability, new_config: LockingConfig, _ctx: &mut TxContext, ) { - // TODO: check_permission(trail, cap, &permissions::locking_update(), ctx); + assert!(trail.has_capability_permission(cap, &permission::update_locking_config()), EPermissionDenied); trail.locking_config = new_config; } -// ===== Metadata ===== +/// Update the `delete_record_lock` locking configuration +public fun trail_update_locking_config_for_delete_record( + trail: &mut AuditTrail, + cap: &Capability, + new_delete_record_lock: LockingWindow, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::update_locking_config_for_delete_record()), EPermissionDenied); + set_delete_record_lock(&mut trail.locking_config, new_delete_record_lock); +} /// Update the trail's mutable metadata -public fun update_metadata( +public fun trail_update_metadata( trail: &mut AuditTrail, cap: &Capability, new_metadata: Option, @@ -511,6 +516,10 @@ public use fun trail_name as AuditTrail.name; public use fun trail_description as AuditTrail.description; public use fun trail_metadata as AuditTrail.metadata; public use fun trail_locking_config as AuditTrail.locking_config; +public use fun trail_update_locking_config as AuditTrail.update_locking_config; +public use fun trail_is_record_locked as AuditTrail.is_record_locked; +public use fun trail_update_locking_config_for_delete_record as AuditTrail.update_locking_config_for_delete_record; +public use fun trail_update_metadata as AuditTrail.update_metadata; public use fun trail_is_empty as AuditTrail.is_empty; public use fun trail_first_sequence as AuditTrail.first_sequence; public use fun trail_last_sequence as AuditTrail.last_sequence; diff --git a/audit-trail-move/sources/locking.move b/audit-trail-move/sources/locking.move index be7a03b..30c8d9c 100644 --- a/audit-trail-move/sources/locking.move +++ b/audit-trail-move/sources/locking.move @@ -99,6 +99,13 @@ public fun delete_record_lock(config: &LockingConfig): &LockingWindow { &config.delete_record_lock } +// ===== LockingConfig Setters ===== + +/// Set the record deletion locking window +public(package) fun set_delete_record_lock(config: &mut LockingConfig, window: LockingWindow) { + config.delete_record_lock = window; +} + // ===== Locking Logic (LockingWindow) ===== /// Check if a record is locked based on time window diff --git a/audit-trail-move/sources/permission.move b/audit-trail-move/sources/permission.move index 2b5b5ce..d76bbd3 100644 --- a/audit-trail-move/sources/permission.move +++ b/audit-trail-move/sources/permission.move @@ -22,11 +22,12 @@ public enum Permission has copy, drop, store { // --- Locking Config - Proposed role: `LockingAdmin` --- - /// Edit the delete_lock configuration for records - ConfigRecordDeleteLock, - /// Edit the delete_lock configuration for the whole Audit Trail - ConfigTrailDeleteLock, - + /// Update the whole locking configuration + UpdateLockingConfig, + /// Update the delete_record_lock configuration which is part of the locking configuration + UpdateLockingConfigForDeleteRecord, + /// Update the delete_lock configuration for the whole Audit Trail + UpdateLockingConfigForDeleteTrail, // --- Role Management - Proposed role: `RoleAdmin` --- /// Add new roles with associated permissions @@ -102,8 +103,9 @@ public fun record_admin_permissions(): VecSet { /// Create permissions typical used for the `LockingAdmin` role public fun locking_admin_permissions(): VecSet { let mut perms = vec_set::empty(); - perms.insert(config_record_delete_lock()); - perms.insert(config_trail_delete_lock()); + perms.insert(update_locking_config()); + perms.insert(update_locking_config_for_delete_trail()); + perms.insert(update_locking_config_for_delete_record()); perms } @@ -154,14 +156,19 @@ public fun correct_record(): Permission { Permission::CorrectRecord } -/// Returns a permission allowing to edit the delete_lock configuration for records -public fun config_record_delete_lock(): Permission { - Permission::ConfigRecordDeleteLock +/// Returns a permission allowing to update the whole locking configuration +public fun update_locking_config(): Permission { + Permission::UpdateLockingConfig +} + +/// Returns a permission allowing to update the delete_lock configuration for records +public fun update_locking_config_for_delete_record(): Permission { + Permission::UpdateLockingConfigForDeleteRecord } -/// Returns a permission allowing to edit the delete_lock configuration for the whole Audit Trail -public fun config_trail_delete_lock(): Permission { - Permission::ConfigTrailDeleteLock +/// Returns a permission allowing to update the delete_lock configuration for the whole Audit Trail +public fun update_locking_config_for_delete_trail(): Permission { + Permission::UpdateLockingConfigForDeleteTrail } /// Returns a permission allowing to add new roles with associated permissions