Permissions and access control
Hubleto decides in three layers whether a user may see or change something:
- Access to the app and to the controller – may the user open the page at all?
- Permissions of the roles – may the user's role do the operation?
- Access to the record – may the user see this record?
1. Access to a controller
Every controller has a $permission string. It is the full name of the controller class with slashes, e.g. Hubleto/App/Community/Deals/Controllers/Deals. The Renderer checks it before the controller is rendered:
src/Renderer.php (excerpt)
if ($controllerObject->requiresAuthenticatedUser) {
if (!$authProvider->isUserInSession()) {
$this->logger()->info("User not authenticated => SignIn.");
$controllerObject = $this->getController(SignIn::class);
}
}
try {
$permissionManager->setPermission($controllerObject->permission);
if (
$controllerObject->requiresAuthenticatedUser
&& !$controllerObject->permittedForAllUsers
) {
$permissionManager->checkPermission();
The two flags of the controller change the result:
| Property | Default | Meaning |
|---|---|---|
$requiresAuthenticatedUser |
true |
The user must be signed in. Set false for public pages. |
$permittedForAllUsers |
false |
Any signed-in user may open the controller. ApiController sets it to true. |
Permission flags of a controller.
PermissionsManager::granted() then grants the access in this order:
- The user is an administrator (administrator type or administrator role).
- The app is marked
permittedForAllUsers. - The permission starts with
Hubleto/Appand the user has the app enabled in their profile (the list of apps in the user's record). The match is by the namespace prefix, so enablingHubleto\App\Community\Dealsgrants all controllers of the app. - The user's role has the permission (table
permissionsof the Settings app), or the permission is granted to all roles.
apps/Settings/PermissionsManager.php (excerpt)
public function grantedForRole(string $permission, int|string $userRole): bool
{
if (empty($permission)) return TRUE;
$granted = (bool) in_array($permission, (array) ($this->permissionsData[$userRole] ?? []));
if (!$granted) {
}
return $granted;
}
NOTE In practice an app is either enabled for a user or not. To enable an app for a user, an administrator selects it in Settings → Users. To grant access to a single controller, assign its permission string to a role.
Default permissions of an app
In install round 3 the app creates its permissions:
installDefaultPermissions()scansControllers/(including the subfolders) and creates one row inpermissionsfor the$permissionstring of every controller.assignPermissionsToRoles()grants each role the generic framework API controllers that every table and form needs (Table/Describe,Form/Describe,Record/Get,Record/Save,Record/Delete,Record/GetList,Record/Lookup). The pages of the app itself are opened through the enabled app (rule 3 above), not through a role.
Override assignPermissionsToRoles() when a specific role needs a specific controller:
public function assignPermissionsToRoles(): void
{
parent::assignPermissionsToRoles();
/** @var \Hubleto\Framework\Models\RolePermission $mRolePermission */
$mRolePermission = $this->permissionsManager()->createRolePermissionModel();
$mRolePermission->grantPermissionByString(
$idRole,
'Hubleto/App/Custom/CarRental/Controllers/Cars'
);
}
2. Permissions of the table and its operations
The description of a table contains the permissions canCreate, canRead, canUpdate and canDelete. The React table and form read them and hide the Add, Delete and Save controls (and do not load the data without canRead). Model::describeTable() sets all four to true. The framework contains a commented-out variant that would ask the permission manager for <Model>:Read, :Create, :Update and :Delete, so today these permissions are not derived from the roles. Change them in describeTable(). EmailMarketing turns the table into a read-only report when the user groups the data:
apps/EmailMarketing/Models/Recipient.php (excerpt)
if (isset($filters['fGroupBy'])) {
$description->setPermissions(false, false, false, false);
Description\Table::setPermissions($canCreate, $canRead, $canUpdate, $canDelete) changes only the parameters that are not null.
NOTE In the current code
Table::setPermissions()tests$canCreatealso when it setscanRead, sosetPermissions(canRead: false)does not work as expected whencanCreateisnull. Pass all four values, or set$description->permissions['canRead']directly, until this is fixed in the framework.
3. Access to a record: ownership columns
The Hubleto\Erp\RecordManager filters every read query by the ownership columns of the table. The columns are normal columns of your model; there is no special configuration:
| Column | Effect |
|---|---|
id_owner |
The owner of the record sees it. |
id_manager |
The manager of the record sees it. |
id_team |
Members of the team see it. |
shared_with |
Users and teams the record is explicitly shared with. |
Ownership columns.
Add id_owner to a model when users should see only their records. Set its default to the current user, as Customer and Deal do:
apps/Deals/Models/Deal.php (excerpt)
'id_owner' => (new Lookup($this, $this->translate('Owner'), User::class))->setReactComponent('InputUserSelect')->setDefaultValue($this->authProvider()->getUserId()),
Ownership is applied in prepareReadQuery(), which is used by tables, forms, lookups and boards. That is why you must always call parent::prepareReadQuery() and extend its result in your own record managers. A query that you build yourself with $model->record->... is not filtered, so use prepareReadQuery() when you read records on behalf of a user.
apps/Customers/Models/RecordManagers/Customer.php (excerpt)
public function prepareReadQuery(mixed $query = null, int $level = 0, array|null $includeRelations = null): mixed
{
$query = parent::prepareReadQuery($query, $level, $includeRelations);
// Virtual tag count
$query->selectSub(function($sub) {
$sub->from('cross_customer_tags')
->join('customer_tags', 'customer_tags.id', '=', 'cross_customer_tags.id_tag')
->whereColumn('cross_customer_tags.id_customer', 'customers.id')
->selectRaw("COUNT(DISTINCT customer_tags.id)");
}, 'tags_count');
return $query;
}
Checking permissions in your code
// the signed-in user
$idUser = $this->authProvider()->getUserId();
$roles = $this->authProvider()->getUserRoles();
// does the user have a permission?
if ($this->permissionsManager()->granted('Hubleto/App/Community/Deals/Controllers/Deals')) {
// ...
}
// is the app enabled for the active user? (used by the desktop to build the sidebar)
$this->permissionsManager()->isAppPermittedForActiveUser($app);
Related pages
- Models, record managers and migrations — the ownership columns in the data layer
- Services
- Authentication and users
