Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
01546d4
feat: add rate limiting structures to user management schema
Josue19-08 Sep 24, 2025
04df46d
feat: add rate limiting structures to course registry schema
Josue19-08 Sep 24, 2025
65da1b8
feat: add rate limiting error types to user management
Josue19-08 Sep 24, 2025
ed3c45a
feat: add rate limiting error types to course registry
Josue19-08 Sep 24, 2025
b631bab
feat: add rate limiting utility functions for user management
Josue19-08 Sep 24, 2025
6948a32
feat: add rate limiting utility functions for course registry
Josue19-08 Sep 24, 2025
8a45a77
feat: add utils module exports for user management
Josue19-08 Sep 24, 2025
d7843e3
feat: export utils module in user management functions
Josue19-08 Sep 24, 2025
5b74099
feat: export course rate limit utils in course registry functions
Josue19-08 Sep 24, 2025
eb9fdb4
feat: integrate rate limiting validation in create user profile
Josue19-08 Sep 24, 2025
84e88ce
feat: integrate rate limiting validation in create course
Josue19-08 Sep 24, 2025
abc0801
feat: initialize default rate limiting config in admin system
Josue19-08 Sep 24, 2025
4d84714
feat: initialize course rate limiting in access control
Josue19-08 Sep 24, 2025
8673645
fix: update admin config structure in delete user tests
Josue19-08 Sep 24, 2025
7ab80d3
fix: configure permissive rate limits for prerequisite tests
Josue19-08 Sep 24, 2025
ffa1554
refactor: simplify storage utils for better soroban compatibility
Josue19-08 Sep 24, 2025
ef4592c
chore: update test snapshots for add module tests
Josue19-08 Sep 24, 2025
1d818d6
chore: update test snapshots for remove module storage isolation
Josue19-08 Sep 24, 2025
60e29b9
chore: update test snapshots for remove module success
Josue19-08 Sep 24, 2025
194f352
chore: update test snapshots for remove multiple modules
Josue19-08 Sep 24, 2025
6efff14
feat: stage all changes
Josue19-08 Sep 24, 2025
6289ff9
feat: resolve merge conflicts between rate-limiting and main branches
Josue19-08 Sep 26, 2025
55395df
fix: resolve merge conflicts in user_management schema.rs
Josue19-08 Sep 29, 2025
102909c
test: prueba de firma GPG
Josue19-08 Sep 29, 2025
42accbc
test: prueba de firma GPG
Josue19-08 Sep 29, 2025
f73d19c
fix: resolve merge conflicts in create_user_profile - integrate rate …
Josue19-08 Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contracts/course/course_registry/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub enum Error {
InvalidPrice100 = 54,
AlreadyInitialized = 55,
DuplicatePrerequisite = 56,
// Rate limiting errors
CourseRateLimitExceeded = 57,
CourseRateLimitNotConfigured = 58,
}

pub fn handle_error(env: &Env, error: Error) -> ! {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use soroban_sdk::{symbol_short, Address, Env, String, Symbol, IntoVal};

use crate::error::{handle_error, Error};
use crate::schema::Course;
use super::course_rate_limit_utils::initialize_course_rate_limit_config;

const COURSE_KEY: Symbol = symbol_short!("course");

Expand Down Expand Up @@ -67,6 +68,10 @@ pub fn initialize(env: &Env, owner: &Address, user_mgmt_addr: &Address) {
env.storage()
.instance()
.set(&(KEY_USER_MGMT_ADDR,), user_mgmt_addr);

// Initialize rate limiting configuration
initialize_course_rate_limit_config(env);

env.events()
.publish((INIT_ACCESS_CONTROL_EVENT,), (owner, user_mgmt_addr));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 SkillCert

use crate::error::{handle_error, Error};
use crate::schema::{DataKey, CourseRateLimitData, CourseRateLimitConfig, DEFAULT_COURSE_RATE_LIMIT_WINDOW, DEFAULT_MAX_COURSE_CREATIONS_PER_WINDOW};
use soroban_sdk::{Address, Env};

/// Check if the user has exceeded the rate limit for course creation operations.
///
/// This function validates if the caller can perform a course creation operation
/// based on the configured rate limiting rules.
///
/// # Arguments
/// * `env` - The Soroban environment
/// * `creator` - The address attempting to create a course
///
/// # Panics
/// * If rate limit is exceeded
/// * If rate limit configuration is not found
pub fn check_course_creation_rate_limit(env: &Env, creator: &Address) {
// Get rate limit configuration
let config_key = DataKey::CourseRateLimitConfig;
let rate_config = match env
.storage()
.persistent()
.get::<DataKey, CourseRateLimitConfig>(&config_key)
{
Some(config) => config,
None => {
// If no configuration exists, use default
get_default_course_rate_limit_config()
}
};

let current_time = env.ledger().timestamp();
let rate_limit_key = DataKey::CourseRateLimit(creator.clone());

// Get existing rate limit data or create new one
let mut rate_data = match env
.storage()
.persistent()
.get::<DataKey, CourseRateLimitData>(&rate_limit_key)
{
Some(data) => data,
None => CourseRateLimitData {
count: 0,
window_start: current_time,
}
};

// Check if we need to reset the window
if current_time >= rate_data.window_start + rate_config.window_seconds {
// Reset the window
rate_data.count = 0;
rate_data.window_start = current_time;
}

// Check if user has exceeded the rate limit
if rate_data.count >= rate_config.max_courses_per_window {
handle_error(env, Error::CourseRateLimitExceeded);
}

// Increment the count and save
rate_data.count += 1;
env.storage()
.persistent()
.set(&rate_limit_key, &rate_data);
}

/// Get the default rate limiting configuration for course operations.
///
/// This function returns the default rate limiting settings that can be
/// used when initializing the system or when no custom configuration is set.
pub fn get_default_course_rate_limit_config() -> CourseRateLimitConfig {
CourseRateLimitConfig {
window_seconds: DEFAULT_COURSE_RATE_LIMIT_WINDOW,
max_courses_per_window: DEFAULT_MAX_COURSE_CREATIONS_PER_WINDOW,
}
}

/// Initialize the default rate limiting configuration for course operations.
///
/// This function should be called during system initialization to set up
/// the default rate limiting configuration.
///
/// # Arguments
/// * `env` - The Soroban environment
pub fn initialize_course_rate_limit_config(env: &Env) {
let config_key = DataKey::CourseRateLimitConfig;

// Only initialize if not already set
if !env.storage().persistent().has(&config_key) {
let default_config = get_default_course_rate_limit_config();
env.storage()
.persistent()
.set(&config_key, &default_config);
}
}

/// Update the course rate limiting configuration.
///
/// This function allows administrators to modify the rate limiting settings.
///
/// # Arguments
/// * `env` - The Soroban environment
/// * `new_config` - The new rate limiting configuration
pub fn update_course_rate_limit_config(env: &Env, new_config: CourseRateLimitConfig) {
let config_key = DataKey::CourseRateLimitConfig;
env.storage()
.persistent()
.set(&config_key, &new_config);
}

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 SkillCert

use super::utils::{to_lowercase, trim, u32_to_string};
use super::course_rate_limit_utils::check_course_creation_rate_limit;
use soroban_sdk::{symbol_short, Address, Env, String, Symbol, Vec};

use crate::error::{handle_error, Error};
use crate::schema::{Course, CourseLevel};
use crate::functions::utils::{to_lowercase, trim, u32_to_string};

const COURSE_KEY: Symbol = symbol_short!("course");
const TITLE_KEY: Symbol = symbol_short!("title");
Expand All @@ -28,6 +28,9 @@ pub fn create_course(
) -> Course {
creator.require_auth();

// Check rate limiting before proceeding with course creation
check_course_creation_rate_limit(&env, &creator);

// ensure the title is not empty and not just whitespace
let trimmed_title: String = trim(&env, &title);
if title.is_empty() || trimmed_title.is_empty() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ mod tests {

let contract_id = env.register(CourseRegistry, ());
let client = CourseRegistryClient::new(&env, &contract_id);

// Initialize course rate limiting with permissive settings for testing
env.as_contract(&contract_id, || {
use crate::schema::{DataKey, CourseRateLimitConfig};
let permissive_config = CourseRateLimitConfig {
window_seconds: 3600,
max_courses_per_window: 100,
};
let config_key = DataKey::CourseRateLimitConfig;
env.storage().persistent().set(&config_key, &permissive_config);
});

let creator: Address = Address::generate(&env);
let course1 = client.create_course(
Expand Down Expand Up @@ -521,6 +532,17 @@ mod tests {

let contract_id = env.register(CourseRegistry, ());
let client = CourseRegistryClient::new(&env, &contract_id);

// Initialize course rate limiting with permissive settings for testing
env.as_contract(&contract_id, || {
use crate::schema::{DataKey, CourseRateLimitConfig};
let permissive_config = CourseRateLimitConfig {
window_seconds: 3600,
max_courses_per_window: 100,
};
let config_key = DataKey::CourseRateLimitConfig;
env.storage().persistent().set(&config_key, &permissive_config);
});

let creator: Address = Address::generate(&env);
let course1 = client.create_course(
Expand Down
1 change: 1 addition & 0 deletions contracts/course/course_registry/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod contract_versioning;
pub mod create_course;
pub mod create_course_category;
pub mod create_prerequisite;
pub mod course_rate_limit_utils;
pub mod delete_course;
pub mod edit_course;
pub mod edit_goal;
Expand Down
32 changes: 32 additions & 0 deletions contracts/course/course_registry/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub const FILTER_MIN_PRICE: u128 = 500;
pub const MAX_SCAN_ID: u32 = 50;
pub const MAX_EMPTY_CHECKS: u32 = 10;

/// Rate limiting constants for course operations
pub const DEFAULT_COURSE_RATE_LIMIT_WINDOW: u64 = 3600; // 1 hour in seconds
pub const DEFAULT_MAX_COURSE_CREATIONS_PER_WINDOW: u32 = 3; // Max course creations per hour per address

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct CourseModule {
Expand All @@ -30,6 +34,30 @@ pub struct CourseGoal {
pub created_at: u64,
}

/// Rate limiting configuration for course operations.
///
/// Tracks rate limiting settings for spam protection in course creation.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct CourseRateLimitConfig {
/// Time window for rate limiting in seconds
pub window_seconds: u64,
/// Maximum course creations allowed per window
pub max_courses_per_window: u32,
}

/// Rate limiting tracking data for course operations per address.
///
/// Stores the current usage count and window start time for course rate limiting.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct CourseRateLimitData {
/// Current count of course creations in this window
pub count: u32,
/// Timestamp when the current window started
pub window_start: u64,
}

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct CourseCategory {
Expand All @@ -49,6 +77,10 @@ pub enum DataKey {
CategorySeq, // Sequence counter for category IDs
CourseCategory(u128), // Course category by ID
Admins, // List of admin addresses
/// Key for storing course rate limiting configuration
CourseRateLimitConfig,
/// Key for storing course rate limiting data per address: address -> CourseRateLimitData
CourseRateLimit(Address),
}

#[contracttype]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,62 @@
4095
]
],
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"vec": [
{
"symbol": "CourseRateLimitConfig"
}
]
},
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"vec": [
{
"symbol": "CourseRateLimitConfig"
}
]
},
"durability": "persistent",
"val": {
"map": [
{
"key": {
"symbol": "max_courses_per_window"
},
"val": {
"u32": 3
}
},
{
"key": {
"symbol": "window_seconds"
},
"val": {
"u64": 3600
}
}
]
}
}
},
"ext": "v0"
},
4095
]
],
[
{
"contract_data": {
Expand Down
Loading