Permissions refactoring

* Migration: Added groups, privileges, user_groups, group_privileges, improved references
* Models: Added Group, Privilege and integrated it into User
* Replaced old permission handling with new models
This commit is contained in:
Igor Scheller 2022-11-06 12:41:52 +01:00 committed by GitHub
parent 35815b0838
commit 99afe3f651
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 923 additions and 331 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace Database\Factories\Engelsystem\Models;
use Engelsystem\Models\Group;
use Illuminate\Database\Eloquent\Factories\Factory;
class GroupFactory extends Factory
{
/** @var string */
protected $model = Group::class;
/**
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->word(),
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories\Engelsystem\Models;
use Engelsystem\Models\Privilege;
use Illuminate\Database\Eloquent\Factories\Factory;
class PrivilegeFactory extends Factory
{
/** @var string */
protected $model = Privilege::class;
/**
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->word(),
'description' => $this->faker->text(),
];
}
}

View File

@ -0,0 +1,274 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Schema\Blueprint;
use stdClass;
class CreatePrivilegesAndGroupsRelatedTables extends Migration
{
use ChangesReferences;
use Reference;
/**
* Creates the new table, copies the data and drops the old one.
*/
public function up(): void
{
$hasPrevious = $this->schema->hasTable('Privileges');
if ($hasPrevious) {
// Rename because some DBMS handle identifiers case-insensitive
$this->schema->rename('Groups', 'groups_old');
$this->schema->rename('Privileges', 'privileges_old');
}
$this->createNew();
if ($hasPrevious) {
$this->copyOldToNew();
$this->changeReferences(
'groups_old',
'UID',
'groups',
'id'
);
$this->changeReferences(
'privileges_old',
'id',
'privileges',
'id'
);
$this->changeReferences(
'UserGroups',
'id',
'users_groups',
'id'
);
$this->changeReferences(
'GroupPrivileges',
'id',
'group_privileges',
'id'
);
$this->schema->drop('UserGroups');
$this->schema->drop('GroupPrivileges');
$this->schema->drop('groups_old');
$this->schema->drop('privileges_old');
}
}
/**
* Recreates the previous table, copies the data and drops the new one.
*/
public function down(): void
{
// Rename because some DBMS handle identifiers case-insensitive
$this->schema->rename('groups', 'groups_new');
$this->schema->rename('privileges', 'privileges_new');
$this->createOldTable();
$this->copyNewToOld();
$this->changeReferences(
'groups_new',
'id',
'Groups',
'UID',
'integer'
);
$this->changeReferences(
'privileges_new',
'id',
'Privileges',
'id'
);
$this->changeReferences(
'users_groups',
'id',
'UserGroups',
'id'
);
$this->changeReferences(
'group_privileges',
'id',
'GroupPrivileges',
'id'
);
$this->schema->drop('users_groups');
$this->schema->drop('group_privileges');
$this->schema->drop('groups_new');
$this->schema->drop('privileges_new');
}
/**
* @return void
*/
protected function createNew(): void
{
$this->schema->create('groups', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 35)->unique();
});
$this->schema->create('privileges', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 128)->unique();
$table->string('description', 1024);
});
$this->schema->create('users_groups', function (Blueprint $table) {
$table->increments('id');
$this->referencesUser($table)->index();
$this->references($table, 'groups')->index();
});
$this->schema->create('group_privileges', function (Blueprint $table) {
$table->increments('id');
$this->references($table, 'groups')->index();
$this->references($table, 'privileges')->index();
});
}
/**
* @return void
*/
protected function createOldTable(): void
{
$this->schema->create('Groups', function (Blueprint $table) {
$table->string('Name', 35);
$table->integer('UID')->primary();
});
$this->schema->create('Privileges', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 128)->unique();
$table->string('desc', 1024);
});
$this->schema->create('UserGroups', function (Blueprint $table) {
$table->increments('id');
$this->references($table, 'users', 'uid');
$this->references($table, 'Groups', 'group_id', 'UID', false, 'integer')->index();
$table->index(['uid', 'group_id']);
});
$this->schema->create('GroupPrivileges', function (Blueprint $table) {
$table->increments('id');
$this->references($table, 'Groups', 'group_id', 'UID', false, 'integer');
$this->references($table, 'Privileges', 'privilege_id')->index();
$table->index(['group_id', 'privilege_id']);
});
}
/**
* @return void
*/
protected function copyOldToNew(): void
{
$connection = $this->schema->getConnection();
/** @var stdClass[] $records */
$records = $connection
->table('groups_old')
->get();
foreach ($records as $record) {
$connection->table('groups')->insert([
'id' => $record->UID,
'name' => $record->Name,
]);
}
$records = $connection
->table('privileges_old')
->get();
foreach ($records as $record) {
$connection->table('privileges')->insert([
'id' => $record->id,
'name' => $record->name,
'description' => $record->desc,
]);
}
$records = $connection
->table('UserGroups')
->get();
foreach ($records as $record) {
$connection->table('users_groups')->insert([
'id' => $record->id,
'user_id' => $record->uid,
'group_id' => $record->group_id,
]);
}
$records = $connection
->table('GroupPrivileges')
->get();
foreach ($records as $record) {
$connection->table('group_privileges')->insert([
'id' => $record->id,
'group_id' => $record->group_id,
'privilege_id' => $record->privilege_id,
]);
}
}
/**
* @return void
*/
protected function copyNewToOld(): void
{
$connection = $this->schema->getConnection();
/** @var Collection|stdClass[] $records */
$records = $connection
->table('groups_new')
->get();
foreach ($records as $record) {
$connection->table('Groups')->insert([
'Name' => $record->name,
'UID' => $record->id,
]);
}
$records = $connection
->table('privileges_new')
->get();
foreach ($records as $record) {
$connection->table('Privileges')->insert([
'id' => $record->id,
'name' => $record->name,
'desc' => $record->description,
]);
}
$records = $connection
->table('users_groups')
->get();
foreach ($records as $record) {
$connection->table('UserGroups')->insert([
'id' => $record->id,
'uid' => $record->user_id,
'group_id' => $record->group_id,
]);
}
$records = $connection
->table('group_privileges')
->get();
foreach ($records as $record) {
$connection->table('GroupPrivileges')->insert([
'id' => $record->id,
'group_id' => $record->group_id,
'privilege_id' => $record->privilege_id,
]);
}
}
}

View File

@ -11,34 +11,38 @@ trait Reference
/**
* @param Blueprint $table
* @param bool $setPrimary
* @return ColumnDefinition
*/
protected function referencesUser(Blueprint $table, bool $setPrimary = false)
protected function referencesUser(Blueprint $table, bool $setPrimary = false): ColumnDefinition
{
$this->references($table, 'users', null, $setPrimary);
return $this->references($table, 'users', null, null, $setPrimary);
}
/**
* @param Blueprint $table
* @param string $targetTable
* @param string|null $fromColumn
* @param string|null $targetColumn
* @param bool $setPrimary
*
* @param string $type
* @return ColumnDefinition
*/
protected function references(
Blueprint $table,
string $targetTable,
?string $fromColumn = null,
bool $setPrimary = false
?string $targetColumn = null,
bool $setPrimary = false,
string $type = 'unsignedInteger'
): ColumnDefinition {
$fromColumn = $fromColumn ?? Str::singular($targetTable) . '_id';
$col = $table->unsignedInteger($fromColumn);
$col = $table->{$type}($fromColumn);
if ($setPrimary) {
$table->primary($fromColumn);
}
$this->addReference($table, $fromColumn, $targetTable);
$this->addReference($table, $fromColumn, $targetTable, $targetColumn ?: 'id');
return $col;
}
@ -47,11 +51,16 @@ trait Reference
* @param Blueprint $table
* @param string $fromColumn
* @param string $targetTable
* @param string|null $targetColumn
*/
protected function addReference(Blueprint $table, string $fromColumn, string $targetTable)
{
protected function addReference(
Blueprint $table,
string $fromColumn,
string $targetTable,
?string $targetColumn = null
) {
$table->foreign($fromColumn)
->references('id')->on($targetTable)
->references($targetColumn ?: 'id')->on($targetTable)
->onUpdate('cascade')
->onDelete('cascade');
}

View File

@ -251,7 +251,7 @@ function user_controller()
auth()->can('admin_user'),
User_is_freeloader($user_source),
User_angeltypes($user_source->id),
User_groups($user_source->id),
$user_source->groups,
$shifts,
$user->id == $user_source->id,
$tshirt_score,

View File

@ -5,7 +5,6 @@
*/
$includeFiles = [
__DIR__ . '/../includes/sys_auth.php',
__DIR__ . '/../includes/sys_form.php',
__DIR__ . '/../includes/sys_log.php',
__DIR__ . '/../includes/sys_menu.php',
@ -22,7 +21,6 @@ $includeFiles = [
__DIR__ . '/../includes/model/ShiftTypes_model.php',
__DIR__ . '/../includes/model/Stats.php',
__DIR__ . '/../includes/model/UserAngelTypes_model.php',
__DIR__ . '/../includes/model/UserGroups_model.php',
__DIR__ . '/../includes/model/User_model.php',
__DIR__ . '/../includes/model/UserWorkLog_model.php',
__DIR__ . '/../includes/model/ValidationResult.php',

View File

@ -78,7 +78,7 @@ function User_is_AngelType_supporter($user, $angeltype)
return false;
}
$privileges = privileges_for_user($user->id);
$privileges = $user->privileges->pluck('name')->toArray();
return (count(Db::select(
'

View File

@ -1,23 +0,0 @@
<?php
use Engelsystem\Database\Db;
/**
* Returns users groups
*
* @param int $userId
* @return array[]
*/
function User_groups($userId)
{
return Db::select(
'
SELECT `Groups`.*
FROM `UserGroups`
JOIN `Groups` ON `Groups`.`UID`=`UserGroups`.`group_id`
WHERE `UserGroups`.`uid`=?
ORDER BY `UserGroups`.`group_id`
',
[$userId]
);
}

View File

@ -1,6 +1,9 @@
<?php
use Engelsystem\Database\Db;
use Engelsystem\Models\Group;
use Engelsystem\Models\Privilege;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
/**
* @return string
@ -17,18 +20,14 @@ function admin_groups()
{
$html = '';
$request = request();
$groups = Db::select('SELECT * FROM `Groups` ORDER BY `Name`');
/** @var Group[]|Collection $groups */
$groups = Group::query()->orderBy('name')->get();
if (!$request->has('action')) {
$groups_table = [];
foreach ($groups as $group) {
$privileges = Db::select('
SELECT `name`
FROM `GroupPrivileges`
JOIN `Privileges` ON (`GroupPrivileges`.`privilege_id` = `Privileges`.`id`)
WHERE `group_id`=?
ORDER BY `name`
', [$group['UID']]);
/** @var Privilege[]|Collection $privileges */
$privileges = $group->privileges()->orderBy('name')->get();
$privileges_html = [];
foreach ($privileges as $privilege) {
@ -36,12 +35,12 @@ function admin_groups()
}
$groups_table[] = [
'name' => $group['Name'],
'name' => $group->name,
'privileges' => join(', ', $privileges_html),
'actions' => button(
page_link_to(
'admin_groups',
['action' => 'edit', 'id' => $group['UID']]
['action' => 'edit', 'id' => $group->id]
),
__('edit'),
'btn-sm'
@ -65,34 +64,26 @@ function admin_groups()
return error('Incomplete call, missing Groups ID.', true);
}
$group = Db::select('SELECT * FROM `Groups` WHERE `UID`=? LIMIT 1', [$group_id]);
/** @var Group|null $group */
$group = Group::find($group_id);
if (!empty($group)) {
$privileges = Db::select('
SELECT `Privileges`.*, `GroupPrivileges`.`group_id`
FROM `Privileges`
LEFT OUTER JOIN `GroupPrivileges`
ON (
`Privileges`.`id` = `GroupPrivileges`.`privilege_id`
AND `GroupPrivileges`.`group_id`=?
)
ORDER BY `Privileges`.`name`
', [$group_id]);
$privileges = groupPrivilegesWithSelected($group);
$privileges_form = [];
foreach ($privileges as $privilege) {
$privileges_form[] = form_checkbox(
'privileges[]',
$privilege['desc'] . ' (' . $privilege['name'] . ')',
$privilege['group_id'] != '',
$privilege['id'],
'privilege-' . $privilege['name']
$privilege->description . ' (' . $privilege->name . ')',
$privilege->selected != '',
$privilege->id,
'privilege-' . $privilege->name
);
}
$privileges_form[] = form_submit('submit', __('Save'));
$html .= page_with_title(__('Edit group'), [
$html .= page_with_title(__('Edit group') . ' ' . $group->name, [
form(
$privileges_form,
page_link_to('admin_groups', ['action' => 'save', 'id' => $group_id])
page_link_to('admin_groups', ['action' => 'save', 'id' => $group->id])
)
]);
} else {
@ -110,29 +101,21 @@ function admin_groups()
return error('Incomplete call, missing Groups ID.', true);
}
$group = Db::selectOne('SELECT * FROM `Groups` WHERE `UID`=? LIMIT 1', [$group_id]);
/** @var Group|null $group */
$group = Group::find($group_id);
$privileges = $request->request->all('privileges');
if (!empty($group)) {
Db::delete('DELETE FROM `GroupPrivileges` WHERE `group_id`=?', [$group_id]);
$group->privileges()->detach();
$privilege_names = [];
foreach ($privileges as $privilege) {
$privilege = (int)$privilege;
$privilege = Privilege::find($privilege);
if ($privilege) {
$group_privileges_source = Db::selectOne(
'SELECT `name` FROM `Privileges` WHERE `id`=? LIMIT 1',
[$privilege]
);
if (!empty($group_privileges_source)) {
Db::insert(
'INSERT INTO `GroupPrivileges` (`group_id`, `privilege_id`) VALUES (?, ?)',
[$group_id, $privilege]
);
$privilege_names[] = $group_privileges_source['name'];
}
$group->privileges()->attach($privilege);
$privilege_names[] = $privilege->name;
}
}
engelsystem_log(
'Group privileges of group ' . $group['Name']
'Group privileges of group ' . $group->name
. ' edited: ' . join(', ', $privilege_names)
);
throw_redirect(page_link_to('admin_groups'));
@ -144,3 +127,24 @@ function admin_groups()
}
return $html;
}
/**
* @param Group $group
* @return Collection|Privilege[]
*/
function groupPrivilegesWithSelected(Group $group): Collection
{
return Privilege::query()
->join('group_privileges', function ($query) use ($group) {
/** @var JoinClause $query */
$query
->where('privileges.id', '=', $query->raw('group_privileges.privilege_id'))
->where('group_privileges.group_id', $group->id)
;
}, null, null, 'left outer')
->orderBy('name')
->get([
'privileges.*',
'group_privileges.group_id as selected'
]);
}

View File

@ -1,7 +1,9 @@
<?php
use Engelsystem\Database\DB;
use Engelsystem\Models\Group;
use Engelsystem\Models\User\User;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
/**
* @return string
@ -126,20 +128,15 @@ function admin_user()
$html .= '<hr />';
$my_highest_group = DB::selectOne(
'SELECT group_id FROM `UserGroups` WHERE `uid`=? ORDER BY `group_id` DESC LIMIT 1',
[$user->id]
);
/** @var Group $my_highest_group */
$my_highest_group = $user->groups()->orderByDesc('id')->first();
if (!empty($my_highest_group)) {
$my_highest_group = $my_highest_group['group_id'];
$my_highest_group = $my_highest_group->id;
}
$angel_highest_group = DB::selectOne(
'SELECT group_id FROM `UserGroups` WHERE `uid`=? ORDER BY `group_id` DESC LIMIT 1',
[$user_id]
);
$angel_highest_group = $user_source->groups()->orderByDesc('id')->first();
if (!empty($angel_highest_group)) {
$angel_highest_group = $angel_highest_group['group_id'];
$angel_highest_group = $angel_highest_group->id;
}
if (
@ -152,26 +149,11 @@ function admin_user()
$html .= form_csrf();
$html .= '<table>';
$groups = DB::select(
'
SELECT *
FROM `Groups`
LEFT OUTER JOIN `UserGroups` ON (
`UserGroups`.`group_id` = `Groups`.`UID`
AND `UserGroups`.`uid` = ?
)
WHERE `Groups`.`UID` <= ?
ORDER BY `Groups`.`Name`
',
[
$user_id,
$my_highest_group,
]
);
$groups = changeableGroups($my_highest_group, $user_id);
foreach ($groups as $group) {
$html .= '<tr><td><input type="checkbox" name="groups[]" value="' . $group['UID'] . '" '
. ($group['group_id'] != '' ? ' checked="checked"' : '')
. ' /></td><td>' . $group['Name'] . '</td></tr>';
$html .= '<tr><td><input type="checkbox" name="groups[]" value="' . $group->id . '" '
. ($group->selected ? ' checked="checked"' : '')
. ' /></td><td>' . $group->name . '</td></tr>';
}
$html .= '</table><br>';
@ -190,44 +172,26 @@ function admin_user()
} else {
switch ($request->input('action')) {
case 'save_groups':
if ($user_id != $user->id || auth()->can('admin_groups')) {
$my_highest_group = DB::selectOne(
'SELECT * FROM `UserGroups` WHERE `uid`=? ORDER BY `group_id` DESC LIMIT 1',
[$user->id]
);
$angel_highest_group = DB::selectOne(
'SELECT * FROM `UserGroups` WHERE `uid`=? ORDER BY `group_id` DESC LIMIT 1',
[$user_id]
);
$angel = User::findOrFail($user_id);
if ($angel->id != $user->id || auth()->can('admin_groups')) {
/** @var Group $my_highest_group */
$my_highest_group = $user->groups()->orderByDesc('id')->first();
/** @var Group $angel_highest_group */
$angel_highest_group = $angel->groups()->orderByDesc('id')->first();
if (
$my_highest_group
&& (
empty($angel_highest_group)
|| ($my_highest_group['group_id'] >= $angel_highest_group['group_id'])
|| ($my_highest_group->id >= $angel_highest_group->id)
)
) {
$groups_source = DB::select(
'
SELECT *
FROM `Groups`
LEFT OUTER JOIN `UserGroups` ON (
`UserGroups`.`group_id` = `Groups`.`UID`
AND `UserGroups`.`uid` = ?
)
WHERE `Groups`.`UID` <= ?
ORDER BY `Groups`.`Name`
',
[
$user_id,
$my_highest_group['group_id'],
]
);
$groups_source = changeableGroups($my_highest_group->id, $angel->id);
$groups = [];
$grouplist = [];
$groupList = [];
foreach ($groups_source as $group) {
$groups[$group['UID']] = $group;
$grouplist[] = $group['UID'];
$groups[$group->id] = $group;
$groupList[] = $group->id;
}
$groupsRequest = $request->input('groups');
@ -235,20 +199,17 @@ function admin_user()
$groupsRequest = [];
}
DB::delete('DELETE FROM `UserGroups` WHERE `uid`=?', [$user_id]);
$angel->groups()->detach();
$user_groups_info = [];
foreach ($groupsRequest as $group) {
if (in_array($group, $grouplist)) {
DB::insert(
'INSERT INTO `UserGroups` (`uid`, `group_id`) VALUES (?, ?)',
[$user_id, $group]
);
$user_groups_info[] = $groups[$group]['Name'];
if (in_array($group, $groupList)) {
$group = $groups[$group];
$angel->groups()->attach($group);
$user_groups_info[] = $group->name;
}
}
$user_source = User::find($user_id);
engelsystem_log(
'Set groups of ' . User_Nick_render($user_source, true) . ' to: '
'Set groups of ' . User_Nick_render($angel, true) . ' to: '
. join(', ', $user_groups_info)
);
$html .= success('Benutzergruppen gespeichert.', true);
@ -321,3 +282,24 @@ function admin_user()
$html
]);
}
/**
* @param $myHighestGroup
* @param $angelId
* @return Collection|Group[]
*/
function changeableGroups($myHighestGroup, $angelId): Collection
{
return Group::query()
->where('groups.id', '<=', $myHighestGroup)
->join('users_groups', function ($query) use ($angelId) {
/** @var JoinClause $query */
$query->where('users_groups.group_id', '=', $query->raw('groups.id'))
->where('users_groups.user_id', $angelId);
}, null, null, 'left outer')
->orderBy('name')
->get([
'groups.*',
'users_groups.group_id as selected'
]);
}

View File

@ -4,6 +4,7 @@ use Carbon\Carbon;
use Engelsystem\Database\Database;
use Engelsystem\Database\Db;
use Engelsystem\Events\Listener\OAuth2;
use Engelsystem\Models\Group;
use Engelsystem\Models\OAuth;
use Engelsystem\Models\User\Contact;
use Engelsystem\Models\User\PersonalData;
@ -299,7 +300,8 @@ function guest_register()
}
// Assign user-group and set password
Db::insert('INSERT INTO `UserGroups` (`uid`, `group_id`) VALUES (?, 20)', [$user->id]);
$defaultGroup = Group::find(auth()->getDefaultRole());
$user->groups()->attach($defaultGroup);
if ($enable_password) {
auth()->setPassword($user, $request->postData('password'));
}

View File

@ -1,43 +0,0 @@
<?php
use Engelsystem\Database\Db;
/**
* @param int $user_id
* @return array
*/
function privileges_for_user($user_id)
{
$privileges = [];
$user_privileges = Db::select('
SELECT `Privileges`.`name`
FROM `users`
JOIN `UserGroups` ON (`users`.`id` = `UserGroups`.`uid`)
JOIN `GroupPrivileges` ON (`UserGroups`.`group_id` = `GroupPrivileges`.`group_id`)
JOIN `Privileges` ON (`GroupPrivileges`.`privilege_id` = `Privileges`.`id`)
WHERE `users`.`id`=?
', [$user_id]);
foreach ($user_privileges as $user_privilege) {
$privileges[] = $user_privilege['name'];
}
return $privileges;
}
/**
* @param int $group_id
* @return array
*/
function privileges_for_group($group_id)
{
$privileges = [];
$groups_privileges = Db::select('
SELECT `name`
FROM `GroupPrivileges`
JOIN `Privileges` ON (`GroupPrivileges`.`privilege_id` = `Privileges`.`id`)
WHERE `group_id`=?
', [$group_id]);
foreach ($groups_privileges as $guest_privilege) {
$privileges[] = $guest_privilege['name'];
}
return $privileges;
}

View File

@ -1,6 +1,7 @@
<?php
use Carbon\Carbon;
use Engelsystem\Models\Group;
use Engelsystem\Models\Room;
use Engelsystem\Models\User\User;
use Engelsystem\Models\Worklog;
@ -431,7 +432,7 @@ function User_view_worklog(Worklog $worklog, $admin_user_worklog_privilege)
* @param bool $admin_user_privilege
* @param bool $freeloader
* @param array[] $user_angeltypes
* @param array[] $user_groups
* @param Group[] $user_groups
* @param array[] $shifts
* @param bool $its_me
* @param int $tshirt_score
@ -734,14 +735,14 @@ function User_angeltypes_render($user_angeltypes)
}
/**
* @param array[] $user_groups
* @param Group[] $user_groups
* @return string
*/
function User_groups_render($user_groups)
{
$output = [];
foreach ($user_groups as $group) {
$output[] = __($group['Name']);
$output[] = __($group->name);
}
return div('col-md-2', [

View File

@ -3,6 +3,7 @@
namespace Engelsystem\Helpers;
use Carbon\Carbon;
use Engelsystem\Models\Group;
use Engelsystem\Models\User\User;
use Engelsystem\Models\User\User as UserRepository;
use Psr\Http\Message\ServerRequestInterface;
@ -10,26 +11,29 @@ use Symfony\Component\HttpFoundation\Session\Session;
class Authenticator
{
/** @var User */
protected $user = null;
/** @var User|null */
protected ?User $user = null;
/** @var ServerRequestInterface */
protected $request;
protected ServerRequestInterface $request;
/** @var Session */
protected $session;
protected Session $session;
/** @var UserRepository */
protected $userRepository;
protected UserRepository $userRepository;
/** @var string[] */
protected $permissions;
protected array $permissions = [];
/** @var int|string|null */
protected $passwordAlgorithm = PASSWORD_DEFAULT;
/** @var int */
protected $guestRole = 10;
protected int $defaultRole = 20;
/** @var int */
protected int $guestRole = 10;
/**
* @param ServerRequestInterface $request
@ -48,7 +52,7 @@ class Authenticator
*
* @return User|null
*/
public function user()
public function user(): ?User
{
if ($this->user) {
return $this->user;
@ -77,7 +81,7 @@ class Authenticator
* @param string $parameter
* @return User|null
*/
public function apiUser($parameter = 'api_key')
public function apiUser(string $parameter = 'api_key'): ?User
{
if ($this->user) {
return $this->user;
@ -88,6 +92,7 @@ class Authenticator
return null;
}
/** @var User|null $user */
$user = $this
->userRepository
->whereApiKey($params[$parameter])
@ -113,7 +118,7 @@ class Authenticator
$user = $this->user();
if ($user) {
$this->permissions = $this->getPermissionsByUser($user);
$this->permissions = $user->privileges->pluck('name')->toArray();
$user->last_login_at = new Carbon();
$user->save();
@ -122,7 +127,9 @@ class Authenticator
}
if (empty($this->permissions)) {
$this->permissions = $this->getPermissionsByGroup($this->guestRole);
/** @var Group $group */
$group = Group::find($this->guestRole);
$this->permissions = $group->privileges->pluck('name')->toArray();
}
}
@ -140,7 +147,7 @@ class Authenticator
* @param string $password
* @return User|null
*/
public function authenticate(string $login, string $password)
public function authenticate(string $login, string $password): ?User
{
/** @var User $user */
$user = $this->userRepository->whereName($login)->first();
@ -164,7 +171,7 @@ class Authenticator
* @param string $password
* @return bool
*/
public function verifyPassword(User $user, string $password)
public function verifyPassword(User $user, string $password): bool
{
if (!password_verify($password, $user->password)) {
return false;
@ -206,7 +213,23 @@ class Authenticator
/**
* @return int
*/
public function getGuestRole()
public function getDefaultRole(): int
{
return $this->defaultRole;
}
/**
* @param int $defaultRole
*/
public function setDefaultRole(int $defaultRole)
{
$this->defaultRole = $defaultRole;
}
/**
* @return int
*/
public function getGuestRole(): int
{
return $this->guestRole;
}
@ -218,24 +241,4 @@ class Authenticator
{
$this->guestRole = $guestRole;
}
/**
* @param User $user
* @return array
* @codeCoverageIgnore
*/
protected function getPermissionsByUser($user)
{
return privileges_for_user($user->id);
}
/**
* @param int $groupId
* @return array
* @codeCoverageIgnore
*/
protected function getPermissionsByGroup(int $groupId)
{
return privileges_for_group($groupId);
}
}

View File

@ -15,6 +15,7 @@ class AuthenticatorServiceProvider extends ServiceProvider
$authenticator = $this->app->make(Authenticator::class);
$authenticator->setPasswordAlgorithm($config->get('password_algorithm'));
$authenticator->setGuestRole($config->get('auth_guest_role', $authenticator->getGuestRole()));
$authenticator->setDefaultRole($config->get('auth_default_role', $authenticator->getDefaultRole()));
$this->app->instance(Authenticator::class, $authenticator);
$this->app->instance('authenticator', $authenticator);

47
src/Models/Group.php Normal file
View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Models;
use Engelsystem\Models\User\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
* @property string $name
*
* @property-read Collection|Privilege[] $privileges
* @property-read Collection|User[] $users
*
* @method static Builder|Group whereId($value)
* @method static Builder|Group whereName($value)
*/
class Group extends BaseModel
{
use HasFactory;
/** @var string[] */
protected $fillable = [
'name',
];
/**
* @return BelongsToMany
*/
public function privileges(): BelongsToMany
{
return $this->belongsToMany(Privilege::class, 'group_privileges');
}
/**
* @return BelongsToMany
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'users_groups');
}
}

40
src/Models/Privilege.php Normal file
View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
* @property string $name
* @property string $description
*
* @property-read Collection|Group[] $groups
*
* @method static Builder|Privilege whereId($value)
* @method static Builder|Privilege whereName($value)
* @method static Builder|Privilege whereDescription($value)
*/
class Privilege extends BaseModel
{
use HasFactory;
/** @var string[] */
protected $fillable = [
'name',
'description',
];
/**
* @return BelongsToMany
*/
public function groups(): BelongsToMany
{
return $this->belongsToMany(Group::class, 'group_privileges');
}
}

View File

@ -4,17 +4,22 @@ namespace Engelsystem\Models\User;
use Carbon\Carbon;
use Engelsystem\Models\BaseModel;
use Engelsystem\Models\Group;
use Engelsystem\Models\Message;
use Engelsystem\Models\News;
use Engelsystem\Models\NewsComment;
use Engelsystem\Models\OAuth;
use Engelsystem\Models\Privilege;
use Engelsystem\Models\Question;
use Engelsystem\Models\Worklog;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection as SupportCollection;
/**
* @property int $id
@ -31,16 +36,19 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
* @property-read QueryBuilder|PersonalData $personalData
* @property-read QueryBuilder|Settings $settings
* @property-read QueryBuilder|State $state
*
* @property-read Collection|Group[] $groups
* @property-read Collection|News[] $news
* @property-read Collection|NewsComment[] $newsComments
* @property-read Collection|OAuth[] $oauth
* @property-read SupportCollection|Privilege[] $privileges
* @property-read Collection|Worklog[] $worklogs
* @property-read Collection|Worklog[] $worklogsCreated
* @property-read int|null $news_count
* @property-read int|null $news_comments_count
* @property-read int|null $oauth_count
* @property-read int|null $worklogs_count
* @property-read int|null $worklogs_created_count
* @property-read Collection|Question[] $questionsAsked
* @property-read Collection|Question[] $questionsAnswered
* @property-read Collection|Message[] $messagesReceived
* @property-read Collection|Message[] $messagesSent
* @property-read Collection|Message[] $messages
*
* @method static QueryBuilder|User[] whereId($value)
* @method static QueryBuilder|User[] whereName($value)
@ -50,12 +58,6 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
* @method static QueryBuilder|User[] whereLastLoginAt($value)
* @method static QueryBuilder|User[] whereCreatedAt($value)
* @method static QueryBuilder|User[] whereUpdatedAt($value)
*
* @property-read Collection|Question[] $questionsAsked
* @property-read Collection|Question[] $questionsAnswered
* @property-read Collection|Message[] $messagesReceived
* @property-read Collection|Message[] $messagesSent
* @property-read Collection|Message[] $messages
*/
class User extends BaseModel
{
@ -94,6 +96,14 @@ class User extends BaseModel
->withDefault();
}
/**
* @return BelongsToMany
*/
public function groups(): BelongsToMany
{
return $this->belongsToMany(Group::class, 'users_groups');
}
/**
* @return HasOne
*/
@ -104,6 +114,33 @@ class User extends BaseModel
->withDefault();
}
/**
* @return Builder
*/
public function privileges(): Builder
{
/** @var Builder $builder */
$builder = Privilege::query()
->whereIn('id', function ($query) {
/** @var QueryBuilder $query */
$query->select('privilege_id')
->from('group_privileges')
->join('users_groups', 'users_groups.group_id', '=', 'group_privileges.group_id')
->where('users_groups.user_id', '=', $this->id)
->distinct();
});
return $builder;
}
/**
* @return SupportCollection
*/
public function getPrivilegesAttribute(): SupportCollection
{
return $this->privileges()->get();
}
/**
* @return HasOne
*/

View File

@ -7,6 +7,7 @@ use Engelsystem\Controllers\HomeController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Response;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
@ -21,7 +22,7 @@ class HomeControllerTest extends TestCase
$config = new Config(['home_site' => '/foo']);
/** @var Authenticator|MockObject $auth */
$auth = $this->createMock(Authenticator::class);
$this->setExpects($auth, 'user', null, true);
$this->setExpects($auth, 'user', null, new User());
/** @var Redirector|MockObject $redirect */
$redirect = $this->createMock(Redirector::class);
$this->setExpects($redirect, 'to', ['/foo'], new Response());

View File

@ -20,9 +20,11 @@ class AuthenticatorServiceProviderTest extends ServiceProviderTest
$app = new Application();
$app->bind(ServerRequestInterface::class, Request::class);
$config = new Config();
$config->set('password_algorithm', PASSWORD_DEFAULT);
$config->set('auth_guest_role', 42);
$config = new Config([
'password_algorithm' => PASSWORD_DEFAULT,
'auth_guest_role' => 42,
'auth_default_role' => 1337,
]);
$app->instance('config', $config);
$serviceProvider = new AuthenticatorServiceProvider($app);
@ -36,5 +38,6 @@ class AuthenticatorServiceProviderTest extends ServiceProviderTest
$auth = $app->get(Authenticator::class);
$this->assertEquals(PASSWORD_DEFAULT, $auth->getPasswordAlgorithm());
$this->assertEquals(42, $auth->getGuestRole());
$this->assertEquals(1337, $auth->getDefaultRole());
}
}

View File

@ -3,6 +3,8 @@
namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Models\Group;
use Engelsystem\Models\Privilege;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\Helpers\Stub\UserModelImplementation;
@ -108,14 +110,23 @@ class AuthenticatorTest extends ServiceProviderTest
*/
public function testCan()
{
$this->initDatabase();
/** @var ServerRequestInterface|MockObject $request */
$request = $this->getMockForAbstractClass(ServerRequestInterface::class);
/** @var Session|MockObject $session */
$session = $this->createMock(Session::class);
/** @var UserModelImplementation|MockObject $userRepository */
$userRepository = new UserModelImplementation();
/** @var User|MockObject $user */
$user = $this->createMock(User::class);
/** @var User $user */
$user = User::factory()->create();
/** @var Group $group */
$group = Group::factory()->create();
/** @var Privilege $privilege */
$privilege = Privilege::factory()->create(['name' => 'bar']);
$user->groups()->attach($group);
$group->privileges()->attach($privilege);
$session->expects($this->once())
->method('get')
@ -128,20 +139,14 @@ class AuthenticatorTest extends ServiceProviderTest
/** @var Authenticator|MockObject $auth */
$auth = $this->getMockBuilder(Authenticator::class)
->setConstructorArgs([$request, $session, $userRepository])
->onlyMethods(['getPermissionsByGroup', 'getPermissionsByUser', 'user'])
->onlyMethods(['user'])
->getMock();
$auth->expects($this->exactly(1))
->method('getPermissionsByGroup')
->with(10)
->willReturn([]);
$auth->expects($this->exactly(1))
->method('getPermissionsByUser')
->with($user)
->willReturn(['bar']);
$auth->expects($this->exactly(2))
->method('user')
->willReturnOnConsecutiveCalls(null, $user);
Group::factory()->create(['id' => $auth->getGuestRole()]);
// No user, no permissions
$this->assertFalse($auth->can('foo'));
@ -245,6 +250,18 @@ class AuthenticatorTest extends ServiceProviderTest
$this->assertEquals(PASSWORD_ARGON2I, $auth->getPasswordAlgorithm());
}
/**
* @covers \Engelsystem\Helpers\Authenticator::setDefaultRole
* @covers \Engelsystem\Helpers\Authenticator::getDefaultRole
*/
public function testDefaultRole()
{
$auth = $this->getAuthenticator();
$auth->setDefaultRole(1337);
$this->assertEquals(1337, $auth->getDefaultRole());
}
/**
* @covers \Engelsystem\Helpers\Authenticator::setGuestRole
* @covers \Engelsystem\Helpers\Authenticator::getGuestRole
@ -262,8 +279,7 @@ class AuthenticatorTest extends ServiceProviderTest
*/
protected function getAuthenticator()
{
return new class extends Authenticator
{
return new class extends Authenticator {
/** @noinspection PhpMissingParentConstructorInspection */
public function __construct()
{

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Models;
use Engelsystem\Models\Group;
use Engelsystem\Models\Privilege;
use Engelsystem\Models\User\User;
class GroupTest extends ModelTest
{
/**
* @covers \Engelsystem\Models\Group::privileges
*/
public function testPrivileges(): void
{
/** @var Privilege $privilege1 */
$privilege1 = Privilege::factory()->create();
/** @var Privilege $privilege2 */
$privilege2 = Privilege::factory()->create();
$model = new Group();
$model->name = 'Some Group';
$model->save();
$model->privileges()->attach($privilege1);
$model->privileges()->attach($privilege2);
/** @var Group $savedModel */
$savedModel = Group::first();
$this->assertEquals('Some Group', $savedModel->name);
$this->assertEquals($privilege1->name, $savedModel->privileges[0]->name);
$this->assertEquals($privilege2->name, $savedModel->privileges[1]->name);
}
/**
* @covers \Engelsystem\Models\Group::users
*/
public function testUsers(): void
{
/** @var User $user1 */
$user1 = User::factory()->create();
/** @var User $user2 = */
$user2 = User::factory()->create();
$model = new Group();
$model->name = 'Some Group';
$model->save();
$model->users()->attach($user1);
$model->users()->attach($user2);
/** @var Group $savedModel */
$savedModel = Group::first();
$this->assertEquals($user1->name, $savedModel->users[0]->name);
$this->assertEquals($user2->name, $savedModel->users[1]->name);
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Test\Unit\Models;
use Engelsystem\Models\Group;
use Engelsystem\Models\Privilege;
class PrivilegeTest extends ModelTest
{
/**
* @covers \Engelsystem\Models\Privilege::groups
*/
public function testGroups(): void
{
/** @var Group $group1 */
$group1 = Group::factory()->create();
/** @var Group $group2 */
$group2 = Group::factory()->create();
$model = new Privilege();
$model->name = 'Some Privilege';
$model->description = 'Some long description';
$model->save();
$model->groups()->attach($group1);
$model->groups()->attach($group2);
/** @var Privilege $savedModel */
$savedModel = Privilege::first();
$this->assertEquals('Some Privilege', $savedModel->name);
$this->assertEquals('Some long description', $savedModel->description);
$this->assertEquals($group1->name, $savedModel->groups[0]->name);
$this->assertEquals($group2->name, $savedModel->groups[1]->name);
}
}

View File

@ -5,9 +5,11 @@ namespace Engelsystem\Test\Unit\Models\User;
use Carbon\Carbon;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Engelsystem\Models\BaseModel;
use Engelsystem\Models\Group;
use Engelsystem\Models\News;
use Engelsystem\Models\NewsComment;
use Engelsystem\Models\OAuth;
use Engelsystem\Models\Privilege;
use Engelsystem\Models\Question;
use Engelsystem\Models\User\Contact;
use Engelsystem\Models\User\HasUserModel;
@ -19,6 +21,7 @@ use Engelsystem\Models\User\User;
use Engelsystem\Models\Worklog;
use Engelsystem\Test\Unit\Models\ModelTest;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
class UserTest extends ModelTest
@ -81,6 +84,52 @@ class UserTest extends ModelTest
];
}
/**
* @return array[]
*/
public function hasManyRelationsProvider(): array
{
return [
'news' => [
News::class,
'news',
[
[
'title' => 'Hey hoo',
'text' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.',
'is_meeting' => false,
],
[
'title' => 'Huuhuuu',
'text' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.',
'is_meeting' => true,
],
]
]
];
}
/**
* @return array[]
*/
public function belongsToManyRelationsProvider(): array
{
return [
'groups' => [
Group::class,
'groups',
[
[
'name' => 'Lorem',
],
[
'name' => 'Ipsum',
]
]
]
];
}
/**
* @covers \Engelsystem\Models\User\User::contact
* @covers \Engelsystem\Models\User\User::license
@ -100,9 +149,9 @@ class UserTest extends ModelTest
$user = new User($this->data);
$user->save();
/** @var HasUserModel $contact */
$contact = new $class($data);
$contact->user()
/** @var HasUserModel $instance */
$instance = new $class($data);
$instance->user()
->associate($user)
->save();
@ -135,29 +184,79 @@ class UserTest extends ModelTest
$this->assertEquals($relatedModelIds, $user->{$name}->modelKeys());
}
/**
* @return array[]
* @covers \Engelsystem\Models\User\User::groups
*
* @dataProvider belongsToManyRelationsProvider
*
* @param string $class Class name of the related models
* @param string $name Name of the accessor for the related models
* @param array $modelData List of the related models
*/
public function hasManyRelationsProvider(): array
public function testBelongsToManyRelations(string $class, string $name, array $modelData): void
{
return [
'news' => [
News::class,
'news',
[
[
'title' => 'Hey hoo',
'text' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.',
'is_meeting' => false,
],
[
'title' => 'Huuhuuu',
'text' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.',
'is_meeting' => true,
],
]
]
];
$user = new User($this->data);
$user->save();
$relatedModelIds = [];
foreach ($modelData as $data) {
/** @var BaseModel $model */
$model = $this->app->make($class);
$stored = $model->create($data);
$stored->users()->attach($user);
$relatedModelIds[] = $stored->id;
}
$this->assertEquals($relatedModelIds, $user->{$name}->modelKeys());
}
/**
* @covers \Engelsystem\Models\User\User::privileges
* @covers \Engelsystem\Models\User\User::getPrivilegesAttribute
*/
public function testPrivileges()
{
$user = new User($this->data);
$user->save();
/** @var Group $group1 */
$group1 = Group::factory()->create();
/** @var Group $group2 */
$group2 = Group::factory()->create();
/** @var Group $group3 */
$group3 = Group::factory()->create();
/** @var Privilege $privilege1 */
$privilege1 = Privilege::factory()->create();
/** @var Privilege $privilege2 */
$privilege2 = Privilege::factory()->create();
/** @var Privilege $privilege3 */
$privilege3 = Privilege::factory()->create();
/** @var Privilege $privilege4 */
$privilege4 = Privilege::factory()->create();
$user->groups()->attach($group1);
$user->groups()->attach($group2);
$group1->privileges()->attach($privilege1);
$group1->privileges()->attach($privilege2);
$group2->privileges()->attach($privilege2);
$group2->privileges()->attach($privilege3);
$group3->privileges()->attach($privilege3);
$group3->privileges()->attach($privilege4);
/** @var User $createdUser */
$createdUser = User::first();
$this->assertInstanceOf(Builder::class, $createdUser->privileges());
$privileges = $createdUser->privileges->pluck('name');
$this->assertCount(3, $privileges);
$this->assertContains($privilege1->name, $privileges);
$this->assertContains($privilege2->name, $privileges);
$this->assertContains($privilege3->name, $privileges);
}
/**