Data and security
Sessions, users and permissions
Sessions
Session::create() reads config/sessions.php, and every controller opens the session when it is constructed ($this->Session).
<?php
// config/sessions.php
$CONFIG = [
'Sessions' => [
'handler' => ['engine' => 'DatabaseSession'], // omit to use PHP's file sessions
'defaults' => 'database',
'cookie' => 'mysite_session', // cookie name
'timeout' => 2040, // minutes (session.gc_maxlifetime)
'cookiePath' => '/', // optional
'ini' => ['session.cookie_samesite' => 'Lax'], // optional ini directives
],
];
With DatabaseSession, sessions are stored in the sessions model's table, so they can be listed and closed from the admin panel. Koshkil enables session.use_strict_mode, session.cookie_httponly and, under HTTPS, session.cookie_secure by default.
$this->Session->write('carrito', [15 => 2]);
$this->Session->read('carrito');
$this->Session->check('carrito'); // does it exist?
$this->Session->consume('notice'); // read and delete
$this->Session->delete('carrito');
$this->Session->renew(); // new id (use after login)
$this->Session->destroy();
Users
Users are the core usuarios model (TMUsuarios, table tbl_usuarios). Main fields:
| Field | Contents |
|---|---|
usr_codigo |
Primary key. |
usr_parent |
"Owner" user (0 = main user). Sub-users belong to a main user. |
usr_user, usr_email |
Login identifiers. |
usr_pass |
Password hash (passwordUtils::createHash()). |
usr_nombre, usr_apellido, usr_telefono… |
Personal data. |
usr_estado |
1 = active. |
usr_hash |
Token for account activation and password reset. |
usr_registrado, usr_uvisita, usr_ulogout |
Sign-up, last visit and last logout dates. |
usr_enable_mfa, usr_enabled_mfa |
Second factor (TOTP, see totpUtils). |
Every active plugin can extend the model with a <plugin>.usuarios behavior.
Site and panel sessions
The site and the panel use different session variables, so a user can be logged into one and not the other:
| Area | Base class | Session variable |
|---|---|---|
| Public site | SuperController, FrontAjaxController |
Session.UserHandler.Frontend (default frontend_user) |
| Admin panel | AdminSuperController, AdminAjaxController |
Session.UserHandler.Backend (default backend_user) |
The variable holds the usr_codigo. init() loads the user into $this->usuario and assigns it to the view as $usuario.
Passwords
Koshkil::Uses('sys.tools.utils.passwordUtils');
$hash = passwordUtils::createHash($password); // to store
$ok = passwordUtils::createHash($password, $usuario->usr_pass); // to verify: the hash if it matches, false if not
Warning
createHash() uses salted SHA-1, a legacy scheme. For new systems, consider PHP's password_hash(); the usr_pass field is 40 characters long, so it would have to be widened in the model.
Protecting a controller
class ReportesController extends AdminSuperController {
protected $openAccess = false; // login required (already so in AdminSuperController)
protected $roles = 'Administrador|Contador'; // one of these roles…
protected $rules = 'reportes|reportes.view'; // …or one of these rules
protected $strictRules = 'reportes.edit'; // and this rule is mandatory
}
- Without a session, the controller calls
processLogin()and showslogin.tpl. - With a session,
checkPermissions()checks two things. If either fails, it showscommon/access_denied.tpland does not run the action:- if there are
$strictRules, the user must have one of those rules; - if there are
$rules, the user must have one of the$rolesor one of the$rules. With an empty$roles, the accepted role isSuperusuario.
- if there are
$roles alone is not enough
$roles is only evaluated together with $rules. A controller that declares only $roles does not restrict access by role; in that case check it in init() with $this->usuario->hasRoles(...).
Roles and rules
- A rule is a named permission (
noticias,categorias…). - A role groups rules (
Superusuario,Administrador,ABM Noticias…). - Every rule assigned to a role carries bitwise attributes:
| Attribute | Constant | Value |
|---|---|---|
add |
TMRules::ADD_RECORD |
1 |
edit |
TMRules::EDIT_RECORD |
2 |
delete |
TMRules::DELETE_RECORD |
4 |
view |
TMRules::VIEW_RECORD |
8 |
15 (1+2+4+8) is full access; 8 is read-only.
Permission checks
$u = $this->usuario;
$u->loadPermissions(); // controllers already do this
$u->hasRoles('Administrador|Editor'); // any of the roles
$u->hasRules('noticias|categorias'); // any of the rules
$u->hasRules('noticias&categorias'); // all of them
$u->checkRule('noticias.edit'); // the rule with the "edit" attribute
$u->checkRule('noticias.delete', 'noticias'); // in a plugin's context
$u->hasRoleOrRule('Administrador', 'noticias');
In templates:
{rule_or_role rules="noticias.edit"}
<a href="{Koshkil::getLink('/admin/noticias/editar/'|cat:$noticia->not_codigo)}">Edit</a>
{/rule_or_role}
{rule_or_role roles="Superusuario"}…{/rule_or_role}
Roles and rules are managed in the admin panel, under Sistema → Roles / Reglas / Usuarios. Plugins declare their own in config/permissions.php (see Plugins).
reCAPTCHA
When keys are configured, SuperController adds the Google reCAPTCHA v3 script to every page and recaptcha.js attaches a token to every AJAX request. On the server:
<?php
// config/domains/<domain>/google.php
$CONFIG = [
'Google' => [
'Recaptcha' => [
'SiteKey' => '…',
'SecretKey' => '…',
'MinScore' => 0.5, // optional
],
],
];
// In a FrontAjaxController
if (!$this->recaptchaOk()) {
return $this->jsonize(['status' => 'error', 'message' => $this->recaptchaError()]);
}
Without configured keys, or if Google does not answer, RecaptchaVerifier::verify() lets the request through.
Checklist
- Pass user data as
where()/having()values (they are escaped automatically). In hand-written SQL useKoshkil::escapeString()orintval(); see Models. - Escape output in templates (
{$text|escape}). - Never take the user id from the form: use
$this->usuario->usr_codigo. - Renew the session id after login (
$this->Session->renew()). - Keep
Debug.Levelat 0 in production andconfig/out of any public repository.