Releases and security
3.0.0 - 2026-10-09
Three structural additions:
ValidationForm<F>(validation proven by the type system),ADb(one database handle type, identical in production and in tests) and therunique_testtest builder with itsrunique testcommand. The model DSL is now read by a single crate,runique_dsl, shared by the macro and the CLI. Authentication and the admin went through a full security review, and acargo mutantspass removed dead and duplicate APIs.
Security
- CSRF: an anonymous session's token survived login; it is now rotated at every privilege change (
rotate_csrf_token). - Logout: the session was saved back under the same id;
logout()now flushes it and changes its id. .envflags:ENFORCE_HTTPS=True(orTRUE,1,yes) didn't enforce HTTPS; flags are now read whatever their case, and an unreadable value logs a warning.ENFORCE_HTTPS: the HTTPS redirect was never mounted; it now is, behind a proxy sendingX-Forwarded-Proto.DatabaseConfig: the database password was printed byDebugandSerialize.- Password verification: a malformed stored hash answered faster than a wrong password; both now cost the same.
- CSP: a second, nonce-less header overwrote the one carrying the nonce as soon as
.with_csp()was used. FileField: a plain text value could make the server delete or move any file; only staged uploads are accepted now.- Admin: create, edit and bulk actions wrote every key of the raw request body; all admin writes now go through a single gate (who, which rows, which fields).
- Admin
extra_routes: reachable by any staff member; they now check the right of the operation they declare. - Accounts: deactivating an account or removing a right didn't affect open sessions; accounts and rights are now read from the database on every request.
- Password reset: it reactivated an account the staff had deactivated (now prevented by a
CHECKconstraint), and it left the account's other sessions open (now all closed). - Password reset links: built from the request's
Hostheader, so an attacker could have a victim's token sent to their own site; they now use.with_public_url(), required in production. upload_to_env(): put the server's absolute path in public file URLs.- Debug error page: API keys, secrets and signatures in request headers were shown in clear.
- Built-in admin lists (users, groups, rights): sorting and filtering accepted any column, password hashes included.
- Open redirect: any host starting with
127.(such as127.evil.com) was treated as local; the address is now parsed. - Host validation: a
Hostheader with an unclosed[made the request panic. sanitize_strict: some Unicode characters made it panic, andjavajavascript:script:rebuiltjavascript:after one pass.link()in templates: path parameters weren't URL-encoded, so a value holding/or?changed the route reached.- Admin user creation: a failed password hash created the account with an empty hash; the creation now fails.
Breaking
- Cargo features:
defaultno longer enables every database engine, andpostgres/mysql/sqliteare mutually exclusive. - Dependencies:
argon20.6 andscrypt0.12. ADbreplaces&DatabaseConnectionin every public signature.login()takes the user instead of its fields, and no longer takes the database.logout()clears the whole session, flash messages included.- A single user model,
eihwaz_users:UserEntity,AdminAuth,RuniqueAdminAuth,.auth()and the type parameter ofwith_password_resetare removed. eihwaz_usersgets anactivated_atcolumn and aCHECKconstraint (upgrade SQL in the migration guide).- Permission cache removed: rights are read from the database;
req.load_user_rights()loads them outside the admin. GuardRules: roles are group names (roles([...]),staff(),superuser());GuardContextand the*_rolemethods are removed.with_public_url()replacesPasswordResetConfig::base_url()andAdminConfig::reset_password_url().- Builder: once-only settings (
routes,with_public_url,with_database…) no longer compile when called twice. - Prelude:
tracing::Levelis exported asLogLevel(no clash with a model enum namedLevel). .env:RATE_LIMITING,ALLOWED_HOSTS,RUNIQUE_ENABLE_CACHEandRUNIQUE_USER_TABLEare no longer read;DEBUGis read whatever its case;RuniqueEnvis removed.RUNIQUE_ENV(devorprod,prodwhen unset or unknown) now sets how the application behaves: session cookie, startup checks, template reload, no-cache, default home page, logs, admin hot reload.DEBUGonly shows the detailed error pages, including in production, with a warning at startup. New:is_dev(),config.dev, thedevtemplate variable.- Development (
RUNIQUE_ENV=dev): the public URL is alwayshttp://127.0.0.1:{PORT}, even with.with_public_url(); the request'sHostis never used any more;upgrade-insecure-requestsis removed; pages carry no SRI and static files areno-cache, so edited CSS reloads without a restart. with_allowed_hosts: validates in production as soon as a host is listed, not in development;.enabled(...)still has the last word.Request::is_get/is_post/is_put/is_deleteare removed.request.query::<T>()returnsAppResult<T>: a query string that doesn't fit is a 400.FormField:validateandfinalizeareasync fn.cleaned_enumrelies onFromStr.Prisme::for_testandForms::mark_validatedrequire thetest-utilsfeature.- Flash messages: lowercase CSS classes (
message-success). - Admin:
extra_routesnames itsCrudOperation;ResourcePermissionsand the roles registry are removed;CountFnreceives the column filters. - Model DSL (
runique_dsl): columns NOT NULL by default (nullable),fk()replaced bybelongs_to,auto_nowset by the entity, strict CLI reading,checkbox [enum]as a list. runique migration down/statusare removed (usesea-orm-cli), along with theapplied/folder.- Sign-in:
login()refuses an account that can't sign in (LoginError); the session holds only the user id — read the account fromrequest.user.auth_login,get_usernameandis_admin_authenticatedare removed,get_user_idis internal,activate_pendingis renamedactivate_account. RuniqueUser::password_hashandRuniqueSessionStore::find_by_userare removed.- Constants:
session::SESSION_USER_IS_STAFF_KEY,session::IS_ACTIVEandsession::SESSION_USER_DROITS_KEYmove toadmin_context(user::IS_STAFF,user::IS_ACTIVE,permission::DROITS);SESSION_USER_USERNAME_KEY,SESSION_USER_IS_SUPERUSER_KEYandadmin_context::*::REQUIREDare removed. - Model DSL:
[step]is removed (it was accepted, then ignored);extend!{}accepts onlyeihwaz_usersandeihwaz_groupes(it never compiled on the other framework tables). - Dead and duplicate APIs removed:
no_statics,with_error_handler,SessionConfig,PasswordConfig::oauth,StaticStaging::enable/disable,attach_middlewares,render_withand others listed in the migration guide.
Added
runique_testtest builder andrunique testcommand: business logic tested against a real database, in a transaction always rolled back, with the SQL trace and swallowed errors detected.ValidationForm<F>: a form that can only be obtained by validating it.- Model DSL:
multichoice/checkboxlist fields,i8/i16enums. makemigrations: indexes on foreign keys, tables referencing each other, column lengths (max_length).- Password rehash at sign-in after changing the hashing algorithm.
RuniqueEngine::close_user_sessions,request.public_url(),Lang::from_env(), public type aliases (utils::aliases).GuardRules: an unknown group name is an error instead of a silent refusal.400.html: the page for a malformed request, overridable like404.html.runique create-superuser: the password needs 12 characters, with a lowercase letter, an uppercase letter, a digit and a special character.
Fixed
makemigrations: binary column types, a table order that changed between runs, composite-index columns read asStringin snapshots.derive_form:timestamp_tzdidn't compile,i8/i16produced ani32, types the engine can't read back are refused at compile time, the parser accepted malformed input (table names, duplicates,has_many … asaliases), and attributes were refused on some types but accepted on equivalent ones (min_length,auto_now).many_to_many: wrong SeaORMRelationvariant generated on the junction side.- Form-level
max_size(Forms::field_max_size,RuniqueForm::max_size) always failed: the override never reached the file field. extend!{ table: "eihwaz_users" }didn't carryactivated_at.[max_size: 500KB]was read as 500 MB.- Admin: pagination ignored sidebar filters, editing a user's groups was ignored, foreign-key labels and many-to-many writes failed on MariaDB.
order_by_random()failed on MariaDB / MySQL.- CSRF: a failure showed no message,
.csrf_exempt()brokeRequeston the exempted path, andcsrf.jsnow refreshes the token right before a form is submitted. - Media files: a replaced upload stayed cached for a year.
FileField: blocking disk I/O during uploads, and no fallback when the media folder sits on another filesystem.- Password reset: token generation errors weren't logged.
- Boot checks and two CLI messages were not translated.
orm-only builds (no database driver) didn't compile.503.htmlwas never registered, so the plain fallback page was always shown.compile_error!messages (database features) are now in English.
Dependencies
sea-orm/sea-orm-migration=2.0.4,argon20.6,scrypt0.12, and routine bumps (tower-http,time,tera-contrib,indexmap,fancy-regex,validator,rust_decimal,syn).- New crate
runique_dsl0.1.0 (the model DSL parser);derive_form3.0.0 depends on it. Publish order:runique_dsl→derive_form→runique.