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-trail-move/Move.lock b/audit-trail-move/Move.lock new file mode 100644 index 0000000..b38c76a --- /dev/null +++ b/audit-trail-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/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-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move new file mode 100644 index 0000000..2ff77a6 --- /dev/null +++ b/audit-trail-move/sources/audit_trail.move @@ -0,0 +1,538 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// 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_trail::main; + +use audit_trail::capability::{Self, Capability}; +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}; +use iota::event; +use iota::linked_table::{Self, LinkedTable}; +use iota::vec_map::{Self, VecMap}; +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 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 ===== +const INITIAL_ADMIN_ROLE_NAME: vector = b"Admin"; + +// ===== Core Structures ===== + +/// Metadata set at trail creation (immutable) +public struct TrailImmutableMetadata has copy, drop, store { + name: Option, + description: Option, +} + +/// 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, + /// 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 + issued_capabilities: VecSet, +} + +// ===== Events ===== + +/// Emitted when a new trail is created +public struct AuditTrailCreated has copy, drop { + trail_id: ID, + creator: address, + timestamp: u64, + 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 { + trail_id: ID, + sequence_number: u64, + added_by: address, + 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 +public fun new_trail_metadata( + name: Option, + description: Option, +): TrailImmutableMetadata { + TrailImmutableMetadata { name, description } +} + +// ===== 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, + locking_config: LockingConfig, + trail_metadata: TrailImmutableMetadata, + updatable_metadata: Option, + clock: &Clock, + ctx: &mut TxContext, +): (Capability, ID) { + let creator = ctx.sender(); + let timestamp = clock::timestamp_ms(clock); + + 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::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, + sequence_number: 0, + added_by: creator, + timestamp, + }); + } else { + initial_data.destroy_none(); + }; + + 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, + created_at: timestamp, + record_count, + records, + locking_config, + roles, + immutable_metadata: trail_metadata, + updatable_metadata, + issued_capabilities, + }; + + transfer::share_object(trail); + + event::emit(AuditTrailCreated { + trail_id, + creator, + timestamp, + has_initial_record, + }); + + (admin_cap, trail_id) +} + +public fun initial_admin_role_name(): String { + INITIAL_ADMIN_ROLE_NAME.to_string() +} + +// ===== Record Operations ===== + +/// Add a record to the trail +/// +/// Records are added sequentially with auto-assigned sequence numbers. +public fun trail_add_record( + trail: &mut AuditTrail, + cap: &Capability, + stored_data: D, + record_metadata: Option, + clock: &Clock, + ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::add_record()), EPermissionDenied); + + let caller = ctx.sender(); + let timestamp = clock::timestamp_ms(clock); + let trail_id = object::uid_to_inner(&trail.id); + let sequence_number = trail.record_count; + + let record = record::new( + stored_data, + record_metadata, + sequence_number, + caller, + 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, + }); +} + +// ===== Locking ===== + +/// Check if a record is locked (cannot be deleted) +public fun trail_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); + + locking::is_locked( + &trail.locking_config, + sequence_number, + record::added_at(record), + trail.record_count, + current_time, + ) +} + +/// Update the locking configuration +public fun trail_update_locking_config( + trail: &mut AuditTrail, + cap: &Capability, + new_config: LockingConfig, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::update_locking_config()), EPermissionDenied); + trail.locking_config = new_config; +} + +/// 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 trail_update_metadata( + trail: &mut AuditTrail, + cap: &Capability, + new_metadata: Option, + _ctx: &mut TxContext, +) { + assert!(trail.has_capability_permission(cap, &permission::update_metadata()), EPermissionDenied); + trail.updatable_metadata = new_metadata; +} + +// ===== Trail Query Functions ===== + +/// Get the total number of records in the trail +public fun trail_record_count(trail: &AuditTrail): u64 { + trail.record_count +} + +/// Get the trail creator address +public fun trail_creator(trail: &AuditTrail): address { + trail.creator +} + +/// Get the trail creation timestamp +public fun trail_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 trail_metadata(trail: &AuditTrail): &Option { + &trail.updatable_metadata +} + +/// Get the locking configuration +public fun trail_locking_config(trail: &AuditTrail): &LockingConfig { + &trail.locking_config +} + +/// Check if the trail is empty (no records) +public fun trail_is_empty(trail: &AuditTrail): bool { + linked_table::is_empty(&trail.records) +} + +/// Get the first sequence number (None if empty) +public fun trail_first_sequence(trail: &AuditTrail): Option { + *linked_table::front(&trail.records) +} + +/// Get the last sequence number (None if empty) +public fun trail_last_sequence(trail: &AuditTrail): Option { + *linked_table::back(&trail.records) +} + +// ===== Record Query Functions ===== + +/// Get a record by sequence number +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 trail_has_record(trail: &AuditTrail, sequence_number: u64): bool { + linked_table::contains(&trail.records, sequence_number) +} + +/// 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. +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) +} + +/// 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::add_roles()), 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::delete_roles()), 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::update_roles()), 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, + cap: &Capability, + 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, + role: &String, + ctx: &mut TxContext, +): Capability { + assert!(trail.has_capability_permission(cap, &permission::add_capabilities()), EPermissionDenied); + 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 +/// 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); + trail.issued_capabilities.remove(&cap_to_destroy.id()); + 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::revoke_capabilities()), EPermissionDenied); + trail.issued_capabilities.remove(&cap_to_revoke); +} + +public fun trail_issued_capabilities( + trail: &AuditTrail, +): &VecSet { + &trail.issued_capabilities +} + +// ===== 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; +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; +public use fun trail_get_record as AuditTrail.get_record; +public use fun trail_has_record as AuditTrail.has_record; +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_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; +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 new file mode 100644 index 0000000..bf53c96 --- /dev/null +++ b/audit-trail-move/sources/capability.move @@ -0,0 +1,79 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// 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, + trail_id: ID, + role: String +} + +/// Create a new capability with a specific role +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(package) fun cap_destroy(cap: Capability) { + let Capability { id, role: _role, trail_id: _trail_id } = cap; + 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; +#[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/sources/locking.move b/audit-trail-move/sources/locking.move new file mode 100644 index 0000000..30c8d9c --- /dev/null +++ b/audit-trail-move/sources/locking.move @@ -0,0 +1,167 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Locking configuration for audit trail records +module audit_trail::locking; + +/// 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, +} + +/// 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 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_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 { + delete_record_lock: window_none(), + } +} + +/// Create a locking config with time-based record deletion lock +public fun time_based(seconds: u64): LockingConfig { + LockingConfig { + delete_record_lock: window_time_based(seconds), + } +} + +/// Create a locking config with count-based record deletion lock +public fun count_based(count: u64): LockingConfig { + LockingConfig { + delete_record_lock: window_count_based(count), + } +} + +// ===== LockingWindow Getters ===== + +/// Get the time window in seconds (if set) +public fun time_window_seconds(window: &LockingWindow): &Option { + &window.time_window_seconds +} + +/// Get the count window (if set) +public fun count_window(window: &LockingWindow): &Option { + &window.count_window +} + +// ===== LockingConfig Getters ===== + +/// Get the record deletion locking window +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 +/// +/// Returns true if the record was created within the time window +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 = (*window.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(window: &LockingWindow, sequence_number: u64, total_records: u64): bool { + if (window.count_window.is_none()) { + return false + }; + + 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 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, + record_timestamp: u64, + total_records: u64, + current_time: u64, +): bool { + is_window_locked( + &config.delete_record_lock, + sequence_number, + record_timestamp, + total_records, + current_time, + ) +} diff --git a/audit-trail-move/sources/permission.move b/audit-trail-move/sources/permission.move new file mode 100644 index 0000000..d76bbd3 --- /dev/null +++ b/audit-trail-move/sources/permission.move @@ -0,0 +1,207 @@ +// 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 + DeleteAuditTrail, + + // --- Record Management - Proposed role: `RecordAdmin` --- + /// Add records to the trail + AddRecord, + /// Delete records from the trail + DeleteRecord, + /// Correct existing records in the trail + CorrectRecord, // TODO: Clarify if needed for MVP + + + // --- Locking Config - Proposed role: `LockingAdmin` --- + /// 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 + AddRoles, + /// Update permissions associated with existing roles + UpdateRoles, + /// Delete existing roles + DeleteRoles, + + // --- Capability Management - Proposed role: `CapAdmin` --- + /// Issue new capabilities + AddCapabilities, + /// Revoke existing capabilities + RevokeCapabilities, + + // --- Meta Data related - Proposed role: `MetadataAdmin` --- + /// Update the updatable metadata field + UpdateMetadata, + /// Delete the updatable metadata field + DeleteMetadata, +} + +/// 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(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(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(update_locking_config()); + perms.insert(update_locking_config_for_delete_trail()); + perms.insert(update_locking_config_for_delete_record()); + perms +} + +/// Create permissions typical used for the `RoleAdmin` role +public fun role_admin_permissions(): VecSet { + let mut perms = vec_set::empty(); + 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(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(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 delete_audit_trail(): Permission { + Permission::DeleteAuditTrail +} + +/// Returns a permission allowing to add records to the trail +public fun add_record(): Permission { + Permission::AddRecord +} + +/// Returns a permission allowing to delete records from the trail +public fun delete_record(): Permission { + Permission::DeleteRecord +} + +/// Returns a permission allowing to correct existing records in the trail +public fun correct_record(): Permission { + Permission::CorrectRecord +} + +/// 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 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 +public fun add_roles(): Permission { + Permission::AddRoles +} + +/// Returns a permission allowing to update permissions associated with existing roles +public fun update_roles(): Permission { + Permission::UpdateRoles +} + +/// Returns a permission allowing to delete existing roles +public fun delete_roles(): Permission { + Permission::DeleteRoles +} + +/// Returns a permission allowing to issue new capabilities +public fun add_capabilities(): Permission { + Permission::AddCapabilities +} + +/// Returns a permission allowing to revoke existing capabilities +public fun revoke_capabilities(): Permission { + Permission::RevokeCapabilities +} + +/// Returns a permission allowing to update the updatable_metadata field +public fun update_metadata(): Permission { + Permission::UpdateMetadata +} + +/// Returns a permission allowing to delete the updatable_metadata field +public fun delete_metadata(): Permission { + Permission::DeleteMetadata +} diff --git a/audit-trail-move/sources/record.move b/audit-trail-move/sources/record.move new file mode 100644 index 0000000..06956cc --- /dev/null +++ b/audit-trail-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_trail::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 +} diff --git a/audit-trail-move/tests/capability_tests.move b/audit-trail-move/tests/capability_tests.move new file mode 100644 index 0000000..fea0f99 --- /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(locking::window_count_based(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(locking::window_count_based(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(locking::window_count_based(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(locking::window_count_based(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_audit_trail_tests.move b/audit-trail-move/tests/create_audit_trail_tests.move new file mode 100644 index 0000000..6de3b79 --- /dev/null +++ b/audit-trail-move/tests/create_audit_trail_tests.move @@ -0,0 +1,294 @@ +#[test_only] +/// This module contains comprehensive tests for the AuditTrail creation functionality. +module audit_trail::create_audit_trail_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}; + +/// 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; + let mut scenario = ts::begin(user); + + { + let locking_config = locking::new(locking::window_count_based(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + // Verify capability was created + assert!(admin_cap.role() == initial_admin_role_name(), 0); + assert!(admin_cap.trail_id() == trail_id, 1); + + // Clean up + admin_cap.destroy_for_testing(); + }; + + 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() == initial_time_for_testing(), 3); + assert!(trail.trail_record_count() == 0, 4); + + ts::return_shared(trail); + }; + + 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; + let mut scenario = ts::begin(user); + + { + 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( + &mut scenario, + locking_config, + std::option::some(initial_data) + ); + + // Verify capability + assert!(admin_cap.role() == initial_admin_role_name(), 0); + assert!(admin_cap.trail_id() == trail_id, 1); + + // Clean up + admin_cap.destroy_for_testing(); + }; + + 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() == initial_time_for_testing(), 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); +} + +/// 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; + 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(locking::window_count_based(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 + admin_cap.destroy_for_testing(); + 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); +} + +/// 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; + let mut scenario = ts::begin(user); + + { + 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, + std::option::none() + ); + + // Clean up + admin_cap.destroy_for_testing(); + }; + + 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); +} + +/// 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; + let mut scenario = ts::begin(user); + + let mut trail_ids = vector::empty(); + + // Create first trail + { + let locking_config = locking::new(locking::window_count_based(0)); + let (admin_cap1, trail_id1) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + trail_ids.push_back(trail_id1); + admin_cap1.destroy_for_testing(); + }; + + ts::next_tx(&mut scenario, user); + + // Create second trail + { + let locking_config = locking::new(locking::window_count_based(0)); + let (admin_cap2, trail_id2) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + trail_ids.push_back(trail_id2); + + // Verify trails have different IDs + assert!(trail_ids[0] != trail_ids[1], 0); + + admin_cap2.destroy_for_testing(); + }; + + 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 locking_config = locking::new(locking::window_count_based(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + &mut scenario, + locking_config, + std::option::none() + ); + + // 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); + }; + + // 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::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 + 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 new file mode 100644 index 0000000..c793285 --- /dev/null +++ b/audit-trail-move/tests/permission_tests.move @@ -0,0 +1,113 @@ +#[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::add_record(); + 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::add_record()); + + 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::add_record()); + permission::add(&mut set, permission::delete_record()); + permission::add(&mut set, permission::delete_audit_trail()); + + 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::add_record(), + permission::delete_record(), + permission::update_metadata(), + ]; + let set = permission::from_vec(perms); + + 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] +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::add_record()]; + let set = permission::from_vec(perms); + + assert!(vec_set::size(&set) == 1, 0); + assert!(permission::has_permission(&set, &permission::add_record()), 0); +} + +#[test] +fun test_from_vec_multiple_permission() { + let perms = vector[ + 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::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::update_metadata()), 0); + assert!(permission::has_permission(&perms, &permission::delete_metadata()), 0); + assert!(iota::vec_set::size(&perms) == 2, 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::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 + assert!(vec_set::size(&set) == 2, 0); +} \ No newline at end of file diff --git a/audit-trail-move/tests/role_tests.move b/audit-trail-move/tests/role_tests.move new file mode 100644 index 0000000..e0fc15c --- /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(locking::window_count_based(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 diff --git a/audit-trail-rs/README.md b/audit-trail-rs/README.md new file mode 100644 index 0000000..71a444e --- /dev/null +++ b/audit-trail-rs/README.md @@ -0,0 +1 @@ +# IOTA Audit Trails diff --git a/audit-trails-move/sources/audit_trails.move b/audit-trails-move/sources/audit_trails.move deleted file mode 100644 index 82e1bf1..0000000 --- a/audit-trails-move/sources/audit_trails.move +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2025 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/// Audit Trails - Tamper-proof sequential record chains with RBAC -module audit_trails::audit_trails; - -use iota::clock::Clock; -use iota::vec_map::VecMap; -use iota::vec_set::VecSet; -use std::string::String; - -// ===== Core Structures ===== - -/// Controls when records can be deleted -public struct LockingConfig has copy, drop, store { - time_window_seconds: Option, - count_window: Option, -} - -/// Immutable trail metadata (set at creation) -public struct TrailMetadata has store { - name: Option, - description: Option, -} - -public struct Permission has copy, drop, store {} - -/// Shared audit trail object -public struct AuditTrail has key, store { - id: UID, - locking_config: LockingConfig, - permissions: VecMap>, - immutable_metadata: TrailMetadata, - updatable_metadata: Option, - issued_capabilities: VecSet, - creator: address, - created_at: u64, - record_count: u64, -} - -/// A single record in the audit trail -public struct Record has key, store { - id: UID, - trail_id: ID, - stored_data: D, - record_metadata: Option, - previous_record_id: Option, - sequence_number: u64, - added_by: address, - added_at: u64, -} diff --git a/audit-trails-move/sources/capabilities.move b/audit-trails-move/sources/capabilities.move deleted file mode 100644 index 23172c2..0000000 --- a/audit-trails-move/sources/capabilities.move +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/// Role-based access control capabilities for audit trails -module audit_trails::capabilities; - -use iota::clock::Clock; -use std::string::String; diff --git a/audit-trails-move/sources/permissions.move b/audit-trails-move/sources/permissions.move deleted file mode 100644 index 4da4229..0000000 --- a/audit-trails-move/sources/permissions.move +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) 2025 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/// Permission system for role-based access control -module audit_trails::permissions; diff --git a/audit-trails-rs/README.md b/audit-trails-rs/README.md deleted file mode 100644 index 02dd617..0000000 --- a/audit-trails-rs/README.md +++ /dev/null @@ -1 +0,0 @@ -# IOTA Audit Trails \ No newline at end of file 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 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