The PHP framework behind SitioYa

Koshkil documentation

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 shows login.tpl.
  • With a session, checkPermissions() checks two things. If either fails, it shows common/access_denied.tpl and 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 $roles or one of the $rules. With an empty $roles, the accepted role is Superusuario.

$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 use Koshkil::escapeString() or intval(); 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.Level at 0 in production and config/ out of any public repository.