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-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-trails-move/sources/locking.move b/audit-trail-move/sources/locking.move similarity index 94% rename from audit-trails-move/sources/locking.move rename to audit-trail-move/sources/locking.move index e59fe3d..30c8d9c 100644 --- a/audit-trails-move/sources/locking.move +++ b/audit-trail-move/sources/locking.move @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /// Locking configuration for audit trail records -module audit_trails::locking; +module audit_trail::locking; /// Defines a locking window (time OR count based) public struct LockingWindow has copy, drop, store { @@ -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 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-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/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 0fc8560..0000000 --- a/audit-trails-move/sources/audit_trails.move +++ /dev/null @@ -1,320 +0,0 @@ -// 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_trails::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 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 ===== -#[error] -const ERecordNotFound: vector = b"Record not found at the given sequence number"; - -// ===== 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, - /// Role name -> set of permissions (TODO: implement) - 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, -} - -// ===== 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, -} - -/// 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, -} - -// ===== 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 -public fun create( - initial_data: Option, - initial_record_metadata: Option, - locking_config: LockingConfig, - trail_metadata: TrailImmutableMetadata, - updatable_metadata: Option, - clock: &Clock, - ctx: &mut TxContext, -): 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(); - }; - - // 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_uid, - creator, - created_at: timestamp, - record_count, - records, - locking_config, - roles: vec_map::empty(), - immutable_metadata: trail_metadata, - updatable_metadata, - issued_capabilities: iota::vec_set::empty(), - }; - - transfer::share_object(trail); - - event::emit(AuditTrailCreated { - trail_id, - creator, - timestamp, - has_initial_record, - }); - - trail_id -} - -// ===== Record Operations ===== - -/// 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 add_record( - trail: &mut AuditTrail, - cap: &Capability, - stored_data: D, - record_metadata: Option, - clock: &Clock, - ctx: &mut TxContext, -) { - // TODO: check_permission(trail, cap, &permissions::record_add(), ctx); - - 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 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 -/// -/// TODO: Add capability parameter and permission check once implemented -public fun update_locking_config( - trail: &mut AuditTrail, - cap: &Capability, - new_config: LockingConfig, - _ctx: &mut TxContext, -) { - // TODO: check_permission(trail, cap, &permissions::locking_update(), ctx); - trail.locking_config = new_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); - trail.updatable_metadata = new_metadata; -} - -// ===== Trail 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 name(trail: &AuditTrail): &Option { - &trail.immutable_metadata.name -} - -/// Get the trail description (immutable metadata) -public fun description(trail: &AuditTrail): &Option { - &trail.immutable_metadata.description -} - -/// Get the updatable metadata -public fun metadata(trail: &AuditTrail): &Option { - &trail.updatable_metadata -} - -/// Get the locking configuration -public fun locking_config(trail: &AuditTrail): &LockingConfig { - &trail.locking_config -} - -/// 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 (None if empty) -public fun 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 { - *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) -} diff --git a/audit-trails-move/sources/capabilities.move b/audit-trails-move/sources/capabilities.move deleted file mode 100644 index fe49bc5..0000000 --- a/audit-trails-move/sources/capabilities.move +++ /dev/null @@ -1,35 +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; - -/// 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 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 } -} 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