Skip to content

Commit

Permalink
Merge branch 'mage-os:2.4-develop' into 2.4-develop
Browse files Browse the repository at this point in the history
  • Loading branch information
ProxiBlue authored Jan 23, 2025
2 parents 1a4f7c7 + 8498d19 commit 0d70c35
Show file tree
Hide file tree
Showing 48 changed files with 1,770 additions and 581 deletions.
74 changes: 51 additions & 23 deletions app/code/Magento/Authorization/Model/Acl/Loader/Rule.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
* Copyright 2012 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

Expand Down Expand Up @@ -87,7 +87,7 @@ public function __construct(
public function populateAcl(Acl $acl)
{
$result = $this->applyPermissionsAccordingToRules($acl);
$this->applyDenyPermissionsForMissingRules($acl, ...$result);
$this->denyPermissionsForMissingRules($acl, $result);
}

/**
Expand All @@ -98,56 +98,84 @@ public function populateAcl(Acl $acl)
*/
private function applyPermissionsAccordingToRules(Acl $acl): array
{
$foundResources = $foundDeniedRoles = [];
$appliedRolePermissionsPerResource = [];
foreach ($this->getRulesArray() as $rule) {
$role = $rule['role_id'];
$resource = $rule['resource_id'];
$privileges = !empty($rule['privileges']) ? explode(',', $rule['privileges']) : null;

if ($acl->hasResource($resource)) {
$foundResources[$resource] = $resource;

$appliedRolePermissionsPerResource[$resource]['allow'] =
$appliedRolePermissionsPerResource[$resource]['allow'] ?? [];
$appliedRolePermissionsPerResource[$resource]['deny'] =
$appliedRolePermissionsPerResource[$resource]['deny'] ?? [];

if ($rule['permission'] == 'allow') {
if ($resource === $this->_rootResource->getId()) {
$acl->allow($role, null, $privileges);
}
$acl->allow($role, $resource, $privileges);
$appliedRolePermissionsPerResource[$resource]['allow'][] = $role;
} elseif ($rule['permission'] == 'deny') {
$foundDeniedRoles[$role] = $role;
$acl->deny($role, $resource, $privileges);
$appliedRolePermissionsPerResource[$resource]['deny'][] = $role;
}
}
}
return [$foundResources, $foundDeniedRoles];

return $appliedRolePermissionsPerResource;
}

/**
* Apply deny permissions for missing rules
* Deny permissions for missing rules
*
* For all rules that were not regenerated in authorization_rule table,
* when adding a new module and without re-saving all roles,
* consider not present rules with deny permissions
*
* @param Acl $acl
* @param array $resources
* @param array $deniedRoles
* @param array $appliedRolePermissionsPerResource
* @return void
*/
private function applyDenyPermissionsForMissingRules(
Acl $acl,
array $resources,
array $deniedRoles
private function denyPermissionsForMissingRules(
Acl $acl,
array $appliedRolePermissionsPerResource,
) {
if (count($resources) && count($deniedRoles)
//ignore denying missing permission if all are allowed
&& !(count($resources) === 1 && isset($resources[static::ALLOW_EVERYTHING]))
) {
foreach ($acl->getResources() as $resource) {
if (!isset($resources[$resource])) {
foreach ($deniedRoles as $role) {
$acl->deny($role, $resource, null);
}
$consolidatedDeniedRoleIds = array_unique(
array_merge(
...array_column($appliedRolePermissionsPerResource, 'deny')
)
);

$hasAppliedPermissions = count($appliedRolePermissionsPerResource) > 0;
$hasDeniedRoles = count($consolidatedDeniedRoleIds) > 0;
$allAllowed = count($appliedRolePermissionsPerResource) === 1
&& isset($appliedRolePermissionsPerResource[static::ALLOW_EVERYTHING]);

if ($hasAppliedPermissions && $hasDeniedRoles && !$allAllowed) {
// Add the resources that are not present in the rules at all,
// assuming that they must be denied for all roles by default
$resourcesUndefinedInAuthorizationRules =
array_diff($acl->getResources(), array_keys($appliedRolePermissionsPerResource));
$assumeDeniedRoleListPerResource =
array_fill_keys($resourcesUndefinedInAuthorizationRules, $consolidatedDeniedRoleIds);

// Add the resources that are permitted for one role and not present in others at all,
// assuming that they must be denied for all other roles by default
foreach ($appliedRolePermissionsPerResource as $resource => $permissions) {
$allowedRoles = $permissions['allow'];
$deniedRoles = $permissions['deny'];
$assumedDeniedRoles = array_diff($consolidatedDeniedRoleIds, $allowedRoles, $deniedRoles);
if ($assumedDeniedRoles) {
$assumeDeniedRoleListPerResource[$resource] = $assumedDeniedRoles;
}
}

// Deny permissions for missing rules
foreach ($assumeDeniedRoleListPerResource as $resource => $denyRoles) {
$acl->deny($denyRoles, $resource, null);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php
/**
* Copyright 2024 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Authorization\Test\Unit\Model\Acl\Loader;

use Magento\Authorization\Model\Acl\Loader\Rule;
use Magento\Framework\Acl;
use Magento\Framework\Acl\Data\CacheInterface;
use Magento\Framework\Acl\RootResource;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Serialize\Serializer\Json;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
* @covers \Magento\Authorization\Model\Acl\Loader\Rule
*/
class MissingDeclineRuleTest extends TestCase
{
/**
* @var Rule
*/
private $model;

/**
* @var RootResource
*/
private $rootResource;

/**
* @var ResourceConnection|MockObject
*/
private $resourceMock;

/**
* @var CacheInterface|MockObject
*/
private $aclDataCacheMock;

/**
* @var Json|MockObject
*/
private $serializerMock;

/**
* @inheritDoc
*/
protected function setUp(): void
{
$this->rootResource = new RootResource('Magento_Backend::all');

$this->resourceMock = $this->getMockBuilder(ResourceConnection::class)
->addMethods(['getTable'])
->onlyMethods(['getConnection'])
->disableOriginalConstructor()
->getMock();

$this->aclDataCacheMock = $this->createMock(CacheInterface::class);
$this->serializerMock = $this->createPartialMock(
Json::class,
['serialize', 'unserialize']
);

$this->serializerMock->method('serialize')
->willReturnCallback(
static function ($value) {
return json_encode($value);
}
);

$this->serializerMock->method('unserialize')
->willReturnCallback(
static function ($value) {
return json_decode($value, true);
}
);

$this->model = new Rule(
$this->rootResource,
$this->resourceMock,
$this->aclDataCacheMock,
$this->serializerMock
);
}

/**
* This test ensures that any new resources, which have not been explicitly defined in the authorization_rule table,
* are automatically denied for all roles unless explicitly allowed.
*
* @return void
* @throws Exception
*/
public function testDenyAbsentResources(): void
{
// Vendor_MyModule::menu and Vendor_MyModule::report permissions are not present in the authorization_rules
// table for role 3, and should be denied by default
$authorizationRulesData = [
['rule_id' => 1, 'role_id' => 2, 'resource_id' => 'Magento_Backend::all', 'permission' => 'deny'],
['rule_id' => 2, 'role_id' => 2, 'resource_id' => 'Vendor_MyModule::index', 'permission' => 'allow'],
['rule_id' => 3, 'role_id' => 2, 'resource_id' => 'Vendor_MyModule::menu', 'permission' => 'deny'],
['rule_id' => 4, 'role_id' => 2, 'resource_id' => 'Vendor_MyModule::report', 'permission' => 'deny'],
['rule_id' => 5, 'role_id' => 3, 'resource_id' => 'Magento_Backend::all', 'permission' => 'deny'],
['rule_id' => 6, 'role_id' => 3, 'resource_id' => 'Vendor_MyModule::index', 'permission' => 'allow'],
];

// Vendor_MyModule::configuration is a new resource that has not been defined in the authorization_rules table
// for any role, and should be denied by default
$getAclResourcesData = array_unique(array_column($authorizationRulesData, 'resource_id'));
$getAclResourcesData[] = 'Vendor_MyModule::configuration';

$this->aclDataCacheMock->expects($this->once())
->method('load')
->with(Rule::ACL_RULE_CACHE_KEY)
->willReturn(
json_encode($authorizationRulesData)
);

$aclMock = $this->createMock(Acl::class);
$aclMock->method('hasResource')->willReturn(true);

$aclMock
->expects($this->exactly(2))
->method('allow');

$aclMock
->expects($this->exactly(7))
->method('deny');

$aclMock
->method('getResources')
->willReturn($getAclResourcesData);

$this->model->populateAcl($aclMock);
}
}
37 changes: 17 additions & 20 deletions app/code/Magento/Backend/App/Area/FrontNameResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ public function getFrontName($checkHost = false)
*/
public function isHostBackend()
{
if (!$this->request->getServer('HTTP_HOST')) {
return false;
}

if ($this->scopeConfig->getValue(self::XML_PATH_USE_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE)) {
$backendUrl = $this->scopeConfig->getValue(self::XML_PATH_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE);
} else {
Expand All @@ -132,28 +136,21 @@ public function isHostBackend()
);
}
}
$host = (string) $this->request->getServer('HTTP_HOST', '');
$hostWithPort = $this->getHostWithPort($backendUrl);

return !($hostWithPort === null || $host === '') && stripos($hostWithPort, $host) !== false;
}
$this->uri->parse($backendUrl);
$configuredHost = $this->uri->getHost();
if (!$configuredHost) {
return false;
}

/**
* Get host with port
*
* @param string $url
* @return mixed|string
*/
private function getHostWithPort($url)
{
$this->uri->parse($url);
$scheme = $this->uri->getScheme();
$configuredPort = $this->uri->getPort() ?: ($this->standardPorts[$this->uri->getScheme()] ?? null);
$uri = ($this->request->isSecure() ? 'https' : 'http') . '://' . $this->request->getServer('HTTP_HOST');
$this->uri->parse($uri);
$host = $this->uri->getHost();
$port = $this->uri->getPort();

if (!$port) {
$port = $this->standardPorts[$scheme] ?? null;
if ($configuredPort) {
$configuredHost .= ':' . $configuredPort;
$host .= ':' . ($this->uri->getPort() ?: $this->standardPorts[$this->uri->getScheme()]);
}
return $port !== null ? $host . ':' . $port : $host;

return strcasecmp($configuredHost, $host) === 0;
}
}
Loading

0 comments on commit 0d70c35

Please sign in to comment.