justinholtweb / craft-icecube
Lock entries, assets, categories, and globals behind a password in Craft CMS.
Package info
github.com/justinholtweb/craft-icecube
Type:craft-plugin
pkg:composer/justinholtweb/craft-icecube
Requires
- php: ^8.2
- craftcms/cms: ^5.3.0
Requires (Dev)
- codeception/codeception: ^5.2.0
- codeception/lib-innerbrowser: ^4.1
- codeception/module-asserts: ^3.0.0
- codeception/module-datafactory: ^3.0.0
- codeception/module-phpbrowser: ^3.0.0
- codeception/module-yii2: ^1.1.9
- craftcms/phpstan: dev-main
- fakerphp/faker: ^1.19.0
- league/factory-muffin: ^3.3.0
- phpunit/phpunit: ^10.5
- vlucas/phpdotenv: ^5.4.1
README
Lock entries, assets, categories, and globals behind a password in Craft CMS.
Requirements
- Craft CMS 5.3.0+
- PHP 8.2+
Installation
composer require justinholtweb/craft-icecube php craft plugin/install icecube
Features
- Lock individual entries, assets, categories, and global sets
- Protect against editing, deleting, or both
- Per-lock passwords or a single master password
- Configurable unlock session duration (1-1440 minutes)
- Optional notes on each lock explaining why it's locked
- Admin bypass setting
- Granular user permissions
Configuration
Visit Settings > Icecube in the control panel to configure:
- Master Password -- fallback password when no per-lock password is set
- Admins Bypass -- allow admin users to skip all locks automatically
- Element Types -- enable/disable locking for entries, assets, categories, and globals individually
- Unlock TTL -- how long an unlock session stays valid (default: 10 minutes)
- Failed Attempts Allowed -- wrong passwords before a lockout (default: 5, max 100)
- Lockout Duration -- how long that user waits before retrying (default: 5 minutes)
Permissions
Icecube registers three permissions under Settings > Users > Permissions:
| Permission | Description |
|---|---|
| Manage locks | Create, edit, and delete locks |
| Bypass all locks | Skip locks entirely without entering a password |
| Unlock locked content | Enter a password to unlock content |
Non-admin users need Unlock locked content before they can enter a password; without it they simply can't save or delete locked content. Admins satisfy every permission check automatically, so this only affects non-admins.
Unlock attempts are rate limited: after a number of failed attempts against the same element and action, that user is refused for a cooling-off period. Both the threshold and the duration are configurable in Settings → Icecube (defaults: 5 attempts, 5 minutes). Counting is per user, so one editor's mistyping can't lock out the team, and a successful unlock clears the count.
How It Works
- Create a lock targeting a specific entry, asset, category, or global set
- Choose whether to lock editing, deleting, or both
- Optionally set a per-lock password and add notes
- When a user tries to save or delete a locked element, they'll see an unlock modal prompting for the password
- A successful unlock grants a time-limited session (configurable via Unlock TTL)
Development
Local tooling runs in DDEV (config is gitignored, not part of the distributed plugin):
ddev start ddev composer install ddev composer phpstan # static analysis (level 5) ddev composer test # fast unit tests ddev composer test-integration # Craft-backed integration tests ddev composer test-all # both suites
Test suites
| Suite | Location | Harness | Covers |
|---|---|---|---|
| Unit | tests/unit |
Plain PHPUnit, Yii only | Settings validation, password hashing, translation integrity |
| Integration | tests/integration |
Codeception + Craft test module | Lock resolution, session unlocks, save/delete enforcement, install migration |
The integration suite boots a real Craft application against a throwaway test
database (credentials in tests/.env, defaulting to DDEV). It wipes and reinstalls
that database on every run, and each test runs inside a transaction that is rolled
back afterwards.
Two constraints are worth knowing before adding tests:
- Don't write project config. Anything that applies project config — including
saveSection(),saveEntryType(), andPlugins::savePluginSettings()— deadlocks in this harness and leaves the database holding metadata locks. Build elements in memory instead (new Entry(['id' => 100, 'sectionId' => 10])); the plugin's services only read an element's class and a couple of ids. If a run does hang, recover withDROP DATABASE test; CREATE DATABASE test;. - The charset is pinned to
utf8mb4intests/_craft/config/db.php, because Craft's default collation is incompatible with its defaultutf8charset on MariaDB.
License
Craft License -- see LICENSE.md.