Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow per user permissions + rewrite permissions handling #3514

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions core/classes/Core/NamelessContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
use DI\Container;

class NamelessContainer extends Container
{
private static NamelessContainer $_instance;

public static function getInstance(): NamelessContainer
{
if (!isset(self::$_instance)) {
self::$_instance = new self();
}

self::configure();

return self::$_instance;
}

private static function configure(): void
{
self::$_instance->set(Cache::class, function () {
return new Cache([
'name' => 'nameless',
'extension' => '.cache',
'path' => ROOT_PATH . '/cache/'
]);
});

self::$_instance->set(DB::class, DB::getInstance());
}
}
39 changes: 0 additions & 39 deletions core/classes/Core/PermissionHandler.php

This file was deleted.

16 changes: 3 additions & 13 deletions core/classes/Core/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -467,19 +467,9 @@ public function hasPermission(string $permission): bool {
return false;
}

foreach ($this->getGroups() as $group) {
$permissions = json_decode($group->permissions, true) ?? [];

if (isset($permissions['administrator']) && $permissions['administrator'] == 1) {
return true;
}

if (isset($permissions[$permission]) && $permissions[$permission] == 1) {
return true;
}
}

return false;
return NamelessContainer::getInstance()
->get(PermissionCalculator::class)
->userHasPermission($this, $permission);
}

/**
Expand Down
1 change: 0 additions & 1 deletion core/classes/DTO/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public function __construct(object $row) {
$this->group_username_css = $row->group_username_css;
$this->admin_cp = $row->admin_cp;
$this->staff = $row->staff;
$this->permissions = $row->permissions;
$this->default_group = $row->default_group;
$this->order = $row->order;
$this->force_tfa = $row->force_tfa;
Expand Down
16 changes: 12 additions & 4 deletions core/classes/Database/DatabaseInitialiser.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ public static function runPostUser() {
$instance->initialiseForum();
}

// todo
private function initialiseGroups(): void {
$this->_db->insert('groups', [
'name' => 'Member',
'group_html' => '<span class="badge badge-success">Member</span>',
'permissions' => '{"usercp.messaging":1,"usercp.signature":1,"usercp.nickname":1,"usercp.private_profile":1,"usercp.profile_banner":1}',
'order' => 3
]);

Expand All @@ -42,7 +42,6 @@ private function initialiseGroups(): void {
'group_username_color' => '#ff0000',
'group_username_css' => '',
'admin_cp' => true,
'permissions' => '{"administrator":1,"admincp.core":1,"admincp.core.api":1,"admincp.core.seo":1,"admincp.core.general":1,"admincp.core.avatars":1,"admincp.core.fields":1,"admincp.core.debugging":1,"admincp.core.emails":1,"admincp.core.queue":1,"admincp.core.navigation":1,"admincp.core.announcements":1,"admincp.core.reactions":1,"admincp.core.registration":1,"admincp.core.social_media":1,"admincp.core.terms":1,"admincp.errors":1,"admincp.core.placeholders":1,"admincp.members":1,"admincp.integrations":1,"admincp.integrations.edit":1,"admincp.discord":1,"admincp.minecraft":1,"admincp.minecraft.authme":1,"admincp.minecraft.servers":1,"admincp.minecraft.query_errors":1,"admincp.minecraft.banners":1,"admincp.modules":1,"admincp.pages":1,"admincp.security":1,"admincp.security.acp_logins":1,"admincp.security.template":1,"admincp.styles":1,"admincp.styles.panel_templates":1,"admincp.styles.templates":1,"admincp.styles.templates.edit":1,"admincp.styles.images":1,"admincp.update":1,"admincp.users":1,"admincp.users.edit":1,"admincp.groups":1,"admincp.groups.self":1,"admincp.widgets":1,"modcp.ip_lookup":1,"modcp.punishments":1,"modcp.punishments.warn":1,"modcp.punishments.ban":1,"modcp.punishments.banip":1,"modcp.punishments.revoke":1,"modcp.reports":1,"modcp.profile_banner_reset":1,"usercp.messaging":1,"usercp.signature":1,"admincp.forums":1,"usercp.private_profile":1,"usercp.nickname":1,"usercp.profile_banner":1,"profile.private.bypass":1, "admincp.security.all":1,"admincp.core.hooks":1,"admincp.security.group_sync":1,"admincp.core.emails_mass_message":1,"modcp.punishments.reset_avatar":1,"usercp.gif_avatar":1}',
'order' => 1,
'staff' => true,
]);
Expand All @@ -51,7 +50,6 @@ private function initialiseGroups(): void {
'name' => 'Moderator',
'group_html' => '<span class="badge badge-primary">Moderator</span>',
'admin_cp' => true,
'permissions' => '{"modcp.ip_lookup":1,"modcp.punishments":1,"modcp.punishments.warn":1,"modcp.punishments.ban":1,"modcp.punishments.banip":1,"modcp.punishments.revoke":1,"modcp.reports":1,"admincp.users":1,"modcp.profile_banner_reset":1,"usercp.messaging":1,"usercp.signature":1,"usercp.private_profile":1,"usercp.nickname":1,"usercp.profile_banner":1,"profile.private.bypass":1}',
'order' => 2,
'staff' => true,
]);
Expand All @@ -60,11 +58,21 @@ private function initialiseGroups(): void {
'name' => 'Unconfirmed Member',
'group_html' => '<span class="badge badge-secondary">Unconfirmed Member</span>',
'group_username_color' => '#6c757d',
'permissions' => '{}',
'default_group' => true,
'order' => 4
]);

$permission_cache = NamelessContainer::getInstance()
->get(PermissionCache::class);
foreach (PermissionRegistry::DEFAULT_GROUP_PERMISSIONS as $group_id => $permissions) {
$group = Group::find($group_id);
if ($group === null) {
continue;
}

$permission_cache->upsert(Group::class, $group->id, $permissions);
}

Settings::set('member_list_viewable_groups', json_encode([1, 2, 3, 4]), 'Members');
}

Expand Down
1 change: 1 addition & 0 deletions core/classes/Misc/Report.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public static function create(Language $language, User $user_reporting, User $re
$id = $db->lastId();

// Alert moderators
// todo
$moderator_groups = DB::getInstance()->query('SELECT id FROM nl2_groups WHERE permissions LIKE \'%"modcp.reports":1%\'')->results();

if (count($moderator_groups)) {
Expand Down
97 changes: 97 additions & 0 deletions core/classes/Permissions/PermissionCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/**
* Allows modules to define permissions.
*
* @package NamelessMC\Core
* @author Samerton
* @version 2.0.0-pr8
* @license MIT
*/
class PermissionCache
{
private DB $_db;
private Cache $_cache;

public function __construct(DB $db, Cache $cache)
{
$this->_db = $db;
$this->_cache = $cache;
}

public function getOrLoad(string $permissible, int $id): array
{
$this->_cache->setCache('permission_cache');

if ($this->_cache->isCached($cache_key = $this->cacheKey($permissible, $id))) {
return $this->_cache->retrieve($cache_key);
}

$this->load($permissible, $id);

return $this->_cache->retrieve($cache_key);
}

public function flush(string $permissible = null, int $id = null): void
{
$this->_cache->setCache('permission_cache');

if ($permissible === null || $id === null) {
$this->_cache->eraseAll();
return;
}

if ($this->_cache->isCached($cache_key = $this->cacheKey($permissible, $id))) {
$this->_cache->erase($cache_key);
}
}

public function upsert(string $permissible, int $id, array $permissions): void
{
if (!count($permissions)) {
return;
}

$values_sql = '';
$values = [];
foreach ($permissions as $permission => $value) {
if (!in_array($value, [PermissionTristate::TRUE, PermissionTristate::FALSE, PermissionTristate::INHERIT])) {
continue;
}

$values_sql .= '(?, ?, ?, ?), ';
$values[] = "$permissible";
$values[] = $id;
$values[] = "$permission";
$values[] = $value;
}

$this->_db->query(
'INSERT INTO nl2_permissions (permissible, permissible_id, permission, `value`) VALUES ' . rtrim($values_sql, ', ') . ' ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)',
$values,
);

$this->flush($permissible, $id);
}

private function load(string $permissible, int $id): array
{
$permissions = $this->_db->query(
'SELECT permission, `value` FROM nl2_permissions WHERE permissible = ? AND permissible_id = ?', [$permissible, $id]
)->results();

$loaded_permissions = [];
foreach ($permissions as $permission) {
$loaded_permissions[$permission->permission] = $permission->value;
}

$this->_cache->setCache('permission_cache');
$this->_cache->store($this->cacheKey($permissible, $id), $loaded_permissions);

return $loaded_permissions;
}

private function cacheKey(string $permissible, int $id): string
{
return $permissible . '_' . $id;
}
}
75 changes: 75 additions & 0 deletions core/classes/Permissions/PermissionCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/**
* Allows modules to define permissions.
*
* @package NamelessMC\Core
* @author Samerton
* @version 2.0.0-pr8
* @license MIT
*/
class PermissionCalculator
{
private PermissionCache $_permission_cache;

public function __construct(PermissionCache $permission_cache)
{
$this->_permission_cache = $permission_cache;
}

public function userHasPermission(User $user, string $permission): bool
{
$user_permissions = $this->_permission_cache->getOrLoad(User::class, $user->data()->id);

$result = $user_permissions[$permission] ?? PermissionTristate::INHERIT;

if ($result === PermissionTristate::TRUE) {
return true;
}

if ($result === PermissionTristate::FALSE) {
return false;
}

if ($result === PermissionTristate::INHERIT) {
foreach ($user->getGroups() as $group) {
if ($this->groupHasPermission($group, $permission)) {
return true;
}
}
}

return false;
}

public function groupHasPermission(Group $group, string $permission): bool
{
$result = $this->_permission_cache->getOrLoad(Group::class, $group->id)[$permission] ?? PermissionTristate::INHERIT;

if ($result === PermissionTristate::TRUE) {
return true;
}

if ($result === PermissionTristate::FALSE) {
return false;
}

if ($result === PermissionTristate::INHERIT) {
// if any of the groups with a lower order have the permission set to true or false, then this group inherits that value
$inherit = false;
$lower_order_groups = DB::getInstance()->query('SELECT id FROM nl2_groups WHERE `order` < ? ORDER BY `order`', [$group->order]);
foreach ($lower_order_groups as $lower_order_group) {

Check failure on line 60 in core/classes/Permissions/PermissionCalculator.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 7.4)

Argument of an invalid type DB supplied for foreach, only iterables are supported.

Check failure on line 60 in core/classes/Permissions/PermissionCalculator.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.0)

Argument of an invalid type DB supplied for foreach, only iterables are supported.

Check failure on line 60 in core/classes/Permissions/PermissionCalculator.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.1)

Argument of an invalid type DB supplied for foreach, only iterables are supported.

Check failure on line 60 in core/classes/Permissions/PermissionCalculator.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2)

Argument of an invalid type DB supplied for foreach, only iterables are supported.
$result2 = $this->_permission_cache->getOrLoad(Group::class, $lower_order_group->id)[$permission] ?? PermissionTristate::INHERIT;
if ($result2 === PermissionTristate::TRUE) {
$inherit = true;
break;
} elseif ($result2 === PermissionTristate::FALSE) {
$inherit = false;
break;
}
}
return $inherit;
}

return false;
}
}
50 changes: 50 additions & 0 deletions core/classes/Permissions/PermissionRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/**
* Allows modules to define permissions.
*
* @package NamelessMC\Core
* @author Samerton
* @version 2.0.0-pr8
* @license MIT
*/
class PermissionRegistry {

public const DEFAULT_GROUP_PERMISSIONS = [
// Member
1 => ["usercp.messaging" => PermissionTristate::TRUE, "usercp.signature" => PermissionTristate::TRUE, "usercp.nickname" => PermissionTristate::TRUE, "usercp.private_profile" => PermissionTristate::TRUE, "usercp.profile_banner" => PermissionTristate::TRUE],
// Admin
2 => ["administrator" => PermissionTristate::TRUE, "admincp.core" => PermissionTristate::TRUE, "admincp.core.api" => PermissionTristate::TRUE, "admincp.core.seo" => PermissionTristate::TRUE, "admincp.core.general" => PermissionTristate::TRUE, "admincp.core.avatars" => PermissionTristate::TRUE, "admincp.core.fields" => PermissionTristate::TRUE, "admincp.core.debugging" => PermissionTristate::TRUE, "admincp.core.emails" => PermissionTristate::TRUE, "admincp.core.queue" => PermissionTristate::TRUE, "admincp.core.navigation" => PermissionTristate::TRUE, "admincp.core.announcements" => PermissionTristate::TRUE, "admincp.core.reactions" => PermissionTristate::TRUE, "admincp.core.registration" => PermissionTristate::TRUE, "admincp.core.social_media" => PermissionTristate::TRUE, "admincp.core.terms" => PermissionTristate::TRUE, "admincp.errors" => PermissionTristate::TRUE, "admincp.core.placeholders" => PermissionTristate::TRUE, "admincp.members" => PermissionTristate::TRUE, "admincp.integrations" => PermissionTristate::TRUE, "admincp.integrations.edit" => PermissionTristate::TRUE, "admincp.discord" => PermissionTristate::TRUE, "admincp.minecraft" => PermissionTristate::TRUE, "admincp.minecraft.authme" => PermissionTristate::TRUE, "admincp.minecraft.servers" => PermissionTristate::TRUE, "admincp.minecraft.query_errors" => PermissionTristate::TRUE, "admincp.minecraft.banners" => PermissionTristate::TRUE, "admincp.modules" => PermissionTristate::TRUE, "admincp.pages" => PermissionTristate::TRUE, "admincp.security" => PermissionTristate::TRUE, "admincp.security.acp_logins" => PermissionTristate::TRUE, "admincp.security.template" => PermissionTristate::TRUE, "admincp.styles" => PermissionTristate::TRUE, "admincp.styles.panel_templates" => PermissionTristate::TRUE, "admincp.styles.templates" => PermissionTristate::TRUE, "admincp.styles.templates.edit" => PermissionTristate::TRUE, "admincp.styles.images" => PermissionTristate::TRUE, "admincp.update" => PermissionTristate::TRUE, "admincp.users" => PermissionTristate::TRUE, "admincp.users.edit" => PermissionTristate::TRUE, "admincp.groups" => PermissionTristate::TRUE, "admincp.groups.self" => PermissionTristate::TRUE, "admincp.widgets" => PermissionTristate::TRUE, "modcp.ip_lookup" => PermissionTristate::TRUE, "modcp.punishments" => PermissionTristate::TRUE, "modcp.punishments.warn" => PermissionTristate::TRUE, "modcp.punishments.ban" => PermissionTristate::TRUE, "modcp.punishments.banip" => PermissionTristate::TRUE, "modcp.punishments.revoke" => PermissionTristate::TRUE, "modcp.reports" => PermissionTristate::TRUE, "modcp.profile_banner_reset" => PermissionTristate::TRUE, "usercp.messaging" => PermissionTristate::TRUE, "usercp.signature" => PermissionTristate::TRUE, "admincp.forums" => PermissionTristate::TRUE, "usercp.private_profile" => PermissionTristate::TRUE, "usercp.nickname" => PermissionTristate::TRUE, "usercp.profile_banner" => PermissionTristate::TRUE, "profile.private.bypass", "admincp.security.all" => PermissionTristate::TRUE, "admincp.core.hooks" => PermissionTristate::TRUE, "admincp.security.group_sync" => PermissionTristate::TRUE, "admincp.core.emails_mass_message" => PermissionTristate::TRUE, "modcp.punishments.reset_avatar" => PermissionTristate::TRUE, "usercp.gif_avatar" => PermissionTristate::TRUE],
// Moderator
3 => ["modcp.ip_lookup" => PermissionTristate::TRUE, "modcp.punishments" => PermissionTristate::TRUE, "modcp.punishments.warn" => PermissionTristate::TRUE, "modcp.punishments.ban" => PermissionTristate::TRUE, "modcp.punishments.banip" => PermissionTristate::TRUE, "modcp.punishments.revoke" => PermissionTristate::TRUE, "modcp.reports" => PermissionTristate::TRUE, "admincp.users" => PermissionTristate::TRUE, "modcp.profile_banner_reset" => PermissionTristate::TRUE, "usercp.messaging" => PermissionTristate::TRUE, "usercp.signature" => PermissionTristate::TRUE, "usercp.private_profile" => PermissionTristate::TRUE, "usercp.nickname" => PermissionTristate::TRUE, "usercp.profile_banner" => PermissionTristate::TRUE, "profile.private.bypass" => PermissionTristate::TRUE],
// Unconfirmed Member
4 => [],
];

/**
* @var array<string, array<string, string>> All registered permissions.
*/
private static array $_permissions;

/**
* Register a permission for display in the StaffCP.
*
* @param string $section Permission section to add permission to.
* @param array $permissions List of unique permissions to register.
*/
public static function registerPermissions(string $section, array $permissions): void {
foreach ($permissions as $permission => $title) {
if (!isset(self::$_permissions[$section][$permission])) {
self::$_permissions[$section][$permission] = $title;
}
}
}

/**
* Get all registered permissions.
*
* @return array<string, array<string, string>> Permission array.
*/
public static function getPermissions(): array {
return self::$_permissions;
}
}
Loading
Loading