Skip to content

SealMatch/tusk-protocol

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Seal Match Move - Access Control System

A Move package for managing recruiter access control and candidate view requests on Sui blockchain. This package provides a secure way to manage who can access candidate information through an on-chain access policy system.

Overview

This package consists of two main modules:

  1. access_policy - Manages recruiter permissions and access control
  2. view_request - Handles candidate profile view requests with approval workflow

Deployed Package

Testnet Package ID: 0xb35fbef347e1a4ea13adb7bd0f24f6c9e82117f5715da28dbf8924539bd2178a

Module: access_policy

The access_policy module provides a capability-based access control system for managing recruiter permissions.

Core Structures

AccessPolicy

A shared object that maintains a list of allowed recruiter addresses.

public struct AccessPolicy has key {
    id: UID,
    allowed: vector<address>,
}

AdminCap

A capability object that proves ownership and allows policy modifications.

public struct AdminCap has key, store {
    id: UID,
    policy_id: ID,
}

Functions

create

Creates a new access policy with initial recruiters.

public entry fun create(
    initial_recruiter: address,
    ctx: &mut TxContext
)
  • Creates an AccessPolicy object (shared) - this will be used to verify recruiter access
  • Creates an AdminCap for the creator - this capability is required to add new recruiters
  • Automatically adds the creator (typically the recruitment platform's wallet address) and initial_recruiter to the allowed list

Returns:

  • access_policy_id: The object ID of the created AccessPolicy (shared object)
  • admin_cap_id: The object ID of the AdminCap (owned by the creator)

add_recruiter

Adds a new recruiter to the allowed list.

public entry fun add_recruiter(
    policy: &mut AccessPolicy,
    cap: &AdminCap,
    recruiter: address,
    _ctx: &mut TxContext
)

Required Parameters:

  • policy: The access_policy_id obtained from the create function
  • cap: The admin_cap_id obtained from the create function (must be owned by the transaction sender)
  • recruiter: The address of the new recruiter to be added

Function Behavior:

  • Requires valid AdminCap ownership
  • Validates the capability matches the policy
  • Adds the recruiter address to the allowed list

seal_approve

Validates that the sender is an approved recruiter.

public fun seal_approve(
    policy: &AccessPolicy,
    ctx: &TxContext
)
  • Checks if the transaction sender is in the allowed list
  • Aborts with E_NOT_ALLOWED if unauthorized

Module: view_request

The view_request module manages candidate profile view requests with an approval workflow.

Core Structure

ViewRequest

Represents a request to view a candidate's profile.

public struct ViewRequest has key {
    id: UID,
    recruiter: address,
    candidate_id: vector<u8>,
    status: u8, // 0=Pending, 1=Approved, 2=Rejected
}

Functions

create

Creates a new view request.

public entry fun create(
    recruiter: address,
    candidate_id: vector<u8>,
    ctx: &mut TxContext
)
  • Creates a view request object
  • Initial status is 0 (Pending)
  • Transfers ownership to the transaction sender

approve

Approves a view request.

public entry fun approve(
    req: &mut ViewRequest,
    _ctx: &mut TxContext
)
  • Sets request status to 1 (Approved)

reject

Rejects a view request.

public entry fun reject(
    req: &mut ViewRequest,
    _ctx: &mut TxContext
)
  • Sets request status to 2 (Rejected)

Building and Deploying

Prerequisites

  • Sui CLI >= 1.26.0
  • A funded Sui testnet wallet

Build the Package

sui move build

This will compile the Move modules and generate build artifacts in the build/ directory.

Test the Package

sui move test

Deploy to Testnet

sui client publish --gas-budget 200000000

After successful deployment, save the package ID from the output. The current deployed package ID is:

0xb35fbef347e1a4ea13adb7bd0f24f6c9e82117f5715da28dbf8924539bd2178a

Usage with Sui SDK

Installation

npm install @mysten/sui.js

TypeScript Example

import { SuiClient } from '@mysten/sui.js/client';
import { TransactionBlock } from '@mysten/sui.js/transactions';
import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519';

const PACKAGE_ID = '0xb35fbef347e1a4ea13adb7bd0f24f6c9e82117f5715da28dbf8924539bd2178a';
const MODULE_NAME = 'access_policy';

// Initialize client
const client = new SuiClient({ url: 'https://fullnode.testnet.sui.io:443' });
const keypair = Ed25519Keypair.deriveKeypair('your-mnemonics-here');

// 1. Create Access Policy
// This function should be called by the recruitment platform's wallet
// It creates an AccessPolicy and AdminCap, adding the platform wallet to the allowed list
async function createAccessPolicy(initialRecruiter: string) {
  const tx = new TransactionBlock();

  tx.moveCall({
    target: `${PACKAGE_ID}::${MODULE_NAME}::create`,
    arguments: [
      tx.pure(initialRecruiter, 'address')
    ],
  });

  const result = await client.signAndExecuteTransactionBlock({
    signer: keypair,
    transactionBlock: tx,
    options: {
      showEffects: true,
      showObjectChanges: true,
    },
  });

  console.log('Policy created:', result);

  // Extract the created object IDs from the result
  const createdObjects = result.objectChanges?.filter(
    (obj: any) => obj.type === 'created'
  );

  const accessPolicy = createdObjects?.find(
    (obj: any) => obj.objectType.includes('AccessPolicy')
  );
  const adminCap = createdObjects?.find(
    (obj: any) => obj.objectType.includes('AdminCap')
  );

  console.log('access_policy_id:', accessPolicy?.objectId);
  console.log('admin_cap_id:', adminCap?.objectId);

  return {
    result,
    access_policy_id: accessPolicy?.objectId,
    admin_cap_id: adminCap?.objectId,
  };
}

// 2. Add Recruiter
// Requires access_policy_id and admin_cap_id from the create function
// Only the AdminCap owner can add new recruiters
async function addRecruiter(
  policyId: string,        // access_policy_id from createAccessPolicy
  adminCapId: string,      // admin_cap_id from createAccessPolicy
  recruiterAddress: string
) {
  const tx = new TransactionBlock();

  tx.moveCall({
    target: `${PACKAGE_ID}::${MODULE_NAME}::add_recruiter`,
    arguments: [
      tx.object(policyId),      // AccessPolicy object ID
      tx.object(adminCapId),    // AdminCap object ID (must be owned by signer)
      tx.pure(recruiterAddress, 'address')
    ],
  });

  const result = await client.signAndExecuteTransactionBlock({
    signer: keypair,
    transactionBlock: tx,
  });

  console.log('Recruiter added:', result);
  return result;
}

// 3. Create View Request
async function createViewRequest(
  recruiterAddress: string,
  candidateId: string
) {
  const tx = new TransactionBlock();

  // Convert candidateId to vector<u8>
  const candidateIdBytes = Array.from(
    new TextEncoder().encode(candidateId)
  );

  tx.moveCall({
    target: `${PACKAGE_ID}::view_request::create`,
    arguments: [
      tx.pure(recruiterAddress, 'address'),
      tx.pure(candidateIdBytes, 'vector<u8>')
    ],
  });

  const result = await client.signAndExecuteTransactionBlock({
    signer: keypair,
    transactionBlock: tx,
  });

  console.log('View request created:', result);
  return result;
}

// 4. Approve View Request
async function approveViewRequest(requestId: string) {
  const tx = new TransactionBlock();

  tx.moveCall({
    target: `${PACKAGE_ID}::view_request::approve`,
    arguments: [
      tx.object(requestId)
    ],
  });

  const result = await client.signAndExecuteTransactionBlock({
    signer: keypair,
    transactionBlock: tx,
  });

  console.log('Request approved:', result);
  return result;
}

// 5. Verify Recruiter Access (dry-run)
async function verifyRecruiterAccess(policyId: string) {
  const tx = new TransactionBlock();

  // seal_approve is not an entry function, so we can't call it directly
  // Instead, use it with dry-run to verify access without executing
  tx.moveCall({
    target: `${PACKAGE_ID}::${MODULE_NAME}::seal_approve`,
    arguments: [
      tx.object(policyId)
    ],
  });

  try {
    // Use devInspectTransactionBlock to simulate without executing
    const result = await client.devInspectTransactionBlock({
      sender: await keypair.getPublicKey().toSuiAddress(),
      transactionBlock: tx,
    });

    if (result.effects.status.status === 'success') {
      console.log('Access verified: Sender is an authorized recruiter');
      return true;
    } else {
      console.log('Access denied:', result.effects.status.error);
      return false;
    }
  } catch (error) {
    console.error('Verification failed:', error);
    return false;
  }
}

// 6. Query Access Policy
async function getAccessPolicy(policyId: string) {
  const policy = await client.getObject({
    id: policyId,
    options: {
      showContent: true,
    },
  });

  console.log('Access Policy:', policy);
  return policy;
}

Error Codes

  • E_NOT_ALLOWED (1): The sender is not in the allowed recruiter list
  • E_INVALID_CAP (2): The AdminCap does not match the AccessPolicy

Project Structure

.
├── Move.toml              # Package manifest
├── sources/
│   ├── access_policy.move # Access control module
│   └── view_request.move  # View request module
└── build/                 # Compiled artifacts

Security Considerations

  1. The AccessPolicy is a shared object, allowing multiple parties to read it
  2. Only the AdminCap holder can add new recruiters
  3. The seal_approve function should be called before granting access to sensitive operations
  4. View requests are owned objects, ensuring only the owner can approve/reject them

Development Workflow

Local Testing

# Build
sui move build

# Test
sui move test

# Publish to local network
sui client publish --gas-budget 200000000

Resources

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors