Skip to content

Commit b5de116

Browse files
Merge pull request #1 from aligent/feature/pci_4_compatibility
Feature/pci 4 compatibility
2 parents 77bd2c1 + a960b1c commit b5de116

24 files changed

+834
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
name: Integration Test
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
integration-test:
10+
runs-on: "ubuntu-latest"
11+
12+
# Only run integration tests on feature branches. Simple documentation updates, formatting can be ignored
13+
if: contains(github.head_ref, 'feature')
14+
name: ${{ matrix.job_title }}
15+
strategy:
16+
fail-fast: true
17+
matrix:
18+
magento:
19+
[
20+
"magento/project-community-edition:>=2.4.6 <2.4.7",
21+
"magento/project-community-edition:>=2.4.7 <2.4.8",
22+
]
23+
include:
24+
- magento: magento/project-community-edition:>=2.4.6 <2.4.7
25+
php: 8.2
26+
composer: 2.2
27+
mysql: "mysql:8.0"
28+
elasticsearch: "elasticsearch:7.17.9"
29+
rabbitmq: "rabbitmq:3.12-management"
30+
redis: "redis:7.0"
31+
job_title: "2.4.6"
32+
33+
- magento: magento/project-community-edition:>=2.4.7 <2.4.8
34+
php: 8.3
35+
composer: 2.7
36+
mysql: "mariadb:10.6"
37+
elasticsearch: "elasticsearch:7.17.9"
38+
rabbitmq: "rabbitmq:3.12-management"
39+
redis: "redis:7.2"
40+
job_title: "2.4.7"
41+
42+
services:
43+
elasticsearch:
44+
image: ${{ matrix.elasticsearch }}
45+
env:
46+
discovery.type: single-node
47+
options: >-
48+
--health-cmd "curl http://localhost:9200/_cluster/health"
49+
--health-interval 10s
50+
--health-timeout 5s
51+
--health-retries 10
52+
ports:
53+
- 9200:9200
54+
55+
mysql:
56+
image: ${{ matrix.mysql }}
57+
env:
58+
MYSQL_DATABASE: magento_integration_tests
59+
MYSQL_USER: user
60+
MYSQL_PASSWORD: password
61+
MYSQL_ROOT_PASSWORD: rootpassword
62+
ports:
63+
- 3306:3306
64+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
65+
66+
rabbitmq:
67+
image: ${{ matrix.rabbitmq }}
68+
env:
69+
RABBITMQ_DEFAULT_USER: guest
70+
RABBITMQ_DEFAULT_PASS: guest
71+
ports:
72+
- 5672:5672
73+
- 15672:15672
74+
steps:
75+
- uses: actions/checkout@v4
76+
- name: Set PHP Version
77+
uses: shivammathur/setup-php@v2
78+
with:
79+
php-version: ${{ matrix.php }}
80+
tools: composer:2
81+
coverage: none
82+
83+
- run: composer create-project --repository-url="https://mirror.mage-os.org/" "${{ matrix.magento }}" ../magento2 --no-install
84+
shell: bash
85+
env:
86+
COMPOSER_AUTH: ""
87+
name: Create Magento ${{ matrix.magento }} Project
88+
89+
- uses: graycoreio/github-actions-magento2/get-magento-version@main
90+
id: magento-version
91+
with:
92+
working-directory: "../magento2"
93+
94+
- name: Get Composer Cache Directory
95+
shell: bash
96+
working-directory: "../magento2"
97+
id: composer-cache
98+
run: |
99+
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
100+
101+
- name: "Cache Composer Packages"
102+
uses: actions/cache@v4
103+
with:
104+
key: "composer | v5 | '' | ${{ hashFiles('composer.lock') }}"
105+
path: ${{ steps.composer-cache.outputs.dir }}
106+
107+
- run: composer config repositories.local path $GITHUB_WORKSPACE
108+
name: Add Github Repo for Testing
109+
working-directory: "../magento2"
110+
shell: bash
111+
112+
- run: |
113+
composer config --no-interaction allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
114+
composer config --no-interaction allow-plugins.laminas/laminas-dependency-plugin true
115+
composer config --no-interaction allow-plugins.magento/* true
116+
name: Fixup Composer Plugins
117+
working-directory: "../magento2"
118+
119+
- run: composer require aligent/magento2-pci-4-compatibility "@dev" --no-update && composer install
120+
name: Require and attempt install
121+
working-directory: "../magento2"
122+
shell: bash
123+
env:
124+
COMPOSER_CACHE_DIR: ${{ steps.composer-cache.outputs.dir }}
125+
COMPOSER_AUTH: ${{ secrets.composer_auth }}
126+
127+
- name: Replace Configuration Settings for env
128+
working-directory: ../magento2/dev/tests/integration
129+
run: |
130+
sed -i "s/'db-host' => 'localhost'/'db-host' => '127.0.0.1'/" etc/install-config-mysql.php.dist
131+
sed -i "s/'db-user' => 'root'/'db-user' => 'user'/" etc/install-config-mysql.php.dist
132+
sed -i "s/'db-password' => '123123q'/'db-password' => 'password'/" etc/install-config-mysql.php.dist
133+
sed -i "s/'elasticsearch-host' => 'localhost'/'elasticsearch-host' => '127.0.0.1'/" etc/install-config-mysql.php.dist
134+
sed -i "s/'amqp-host' => 'localhost'/'amqp-host' => '127.0.0.1'/" etc/install-config-mysql.php.dist
135+
136+
- run: ../../../vendor/bin/phpunit ../../../vendor/aligent/magento2-pci-4-compatibility/Test/Integration
137+
working-directory: ../magento2/dev/tests/integration
138+
name: Run Integration Tests
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
namespace Aligent\Pci4Compatibility\Console;
5+
6+
use Aligent\Pci4Compatibility\Model\DisableInactiveAdminAccounts;
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
11+
class DisableInactiveAccounts extends Command
12+
{
13+
14+
/**
15+
* @param DisableInactiveAdminAccounts $disableInactiveAdminAccounts
16+
* @param string|null $name
17+
*/
18+
public function __construct(
19+
private readonly DisableInactiveAdminAccounts $disableInactiveAdminAccounts,
20+
?string $name = null
21+
) {
22+
parent::__construct($name);
23+
}
24+
25+
protected function configure(): void
26+
{
27+
$this->setName('aligent:pci4:disable-inactive-accounts');
28+
$this->setDescription('Disable admin accounts with 90 days of inactivity');
29+
parent::configure();
30+
}
31+
32+
/**
33+
* Disable admin accounts with 90 days of inactivity
34+
*
35+
* Used to be able to satisfy PCI DSS 4.0.1 requirement 8.2.6
36+
*
37+
* @param InputInterface $input
38+
* @param OutputInterface $output
39+
* @return void
40+
*/
41+
protected function execute(InputInterface $input, OutputInterface $output): void
42+
{
43+
$output->writeln(__("Disabling any admin accounts with 90 days of inactivity"));
44+
$this->disableInactiveAdminAccounts->execute();
45+
$output->writeln(__("Disabling of accounts complete"));
46+
}
47+
}

Cron/DisableInactiveAccounts.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
namespace Aligent\Pci4Compatibility\Cron;
5+
6+
use Aligent\Pci4Compatibility\Model\DisableInactiveAdminAccounts;
7+
8+
class DisableInactiveAccounts
9+
{
10+
11+
/**
12+
* @param DisableInactiveAdminAccounts $inactiveUsers
13+
*/
14+
public function __construct(
15+
private readonly DisableInactiveAdminAccounts $inactiveUsers,
16+
) {
17+
}
18+
19+
/**
20+
* Cron job that simply runs service to disable inactive admin accounts.
21+
*
22+
* This is to satisfy PCI DSS 4.0.1 requirement 8.2.6
23+
*
24+
* @return void
25+
*/
26+
public function execute(): void
27+
{
28+
$this->inactiveUsers->execute();
29+
}
30+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
namespace Aligent\Pci4Compatibility\Model;
5+
6+
use DateInterval;
7+
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
8+
use Magento\User\Model\ResourceModel\User as UserResource;
9+
use Magento\User\Model\ResourceModel\User\CollectionFactory as UserCollectionFactory;
10+
use Magento\User\Model\User;
11+
use Psr\Log\LoggerInterface;
12+
13+
class DisableInactiveAdminAccounts
14+
{
15+
private const INACTIVE_DAYS = 90;
16+
17+
/**
18+
* @param TimezoneInterface $localeDate
19+
* @param UserCollectionFactory $userCollectionFactory
20+
* @param UserResource $userResource
21+
* @param LoggerInterface $logger
22+
*/
23+
public function __construct(
24+
private readonly TimezoneInterface $localeDate,
25+
private readonly UserCollectionFactory $userCollectionFactory,
26+
private readonly UserResource $userResource,
27+
private readonly LoggerInterface $logger
28+
) {
29+
}
30+
31+
/**
32+
* Sets the account of any admin user with no login in the last 90 days to inactive.
33+
*
34+
* This is used to be able to satisfy PCI DSS 4.0.1 requirement 8.2.6
35+
*
36+
* @return void
37+
*/
38+
public function execute(): void
39+
{
40+
try {
41+
$currentDate = $this->localeDate->date();
42+
$currentDateUtc = $currentDate->setTimezone(new \DateTimeZone('UTC'));
43+
$utcDateTime = $currentDateUtc->sub(new DateInterval('P' . self::INACTIVE_DAYS . 'D'));
44+
$utcDateTime = $utcDateTime->format('Y-m-d H:i:s');
45+
} catch (\Exception $e) {
46+
$this->logger->critical(
47+
__METHOD__ . ': Could not get cutoff date for inactivity: ' . $e->getMessage(),
48+
['exception' => $e]
49+
);
50+
return;
51+
}
52+
53+
// find users that have been inactive for 90 days
54+
$userCollection = $this->userCollectionFactory->create();
55+
// don't need to disable accounts that are already disabled
56+
$userCollection->addFieldToFilter('is_active', '1');
57+
$userCollection->addFieldToFilter('logdate', ['lt' => $utcDateTime]);
58+
$users = $userCollection->getItems();
59+
foreach ($users as $user) {
60+
/** @var User $user */
61+
$this->disableUserAccount($user);
62+
}
63+
64+
// find user accounts older than 90 days that have never logged in
65+
$userCollection = $this->userCollectionFactory->create();
66+
$userCollection->addFieldToFilter('is_active', '1');
67+
$userCollection->addFieldToFilter('logdate', ['null' => true]);
68+
$userCollection->addFieldToFilter('created', ['lt' => $utcDateTime]);
69+
$users = $userCollection->getItems();
70+
foreach ($users as $user) {
71+
/** @var User $user */
72+
$this->disableUserAccount($user);
73+
}
74+
}
75+
76+
private function disableUserAccount(User $user): void
77+
{
78+
$user->setData('is_active', 0);
79+
$username = $user->getData('username');
80+
try {
81+
$this->userResource->save($user);
82+
$this->logger->info("Account for user $username has been disabled.");
83+
} catch (\Exception $e) {
84+
$this->logger->critical(
85+
__METHOD__ . ": Could not disable account for user $username :" . $e->getMessage(),
86+
['exception' => $e]
87+
);
88+
}
89+
}
90+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
namespace Aligent\Pci4Compatibility\Plugin\Model;
5+
6+
use Magento\Framework\Validator\DataObject;
7+
use Magento\Framework\Validator\NotEmpty;
8+
use Magento\Framework\Validator\Regex;
9+
use Magento\Framework\Validator\StringLength;
10+
use Magento\User\Model\UserValidationRules;
11+
12+
class ReplacePasswordValidationRules
13+
{
14+
15+
private const MIN_PASSWORD_LENGTH = 12;
16+
17+
/**
18+
* Updates the requirements for admin passwords so that the minimum length is 12 characters
19+
*
20+
* @param UserValidationRules $subject
21+
* @param callable $proceed
22+
* @param DataObject $validator
23+
* @return DataObject
24+
*/
25+
public function aroundAddPasswordRules(
26+
UserValidationRules $subject,
27+
callable $proceed,
28+
DataObject $validator
29+
): DataObject {
30+
$passwordNotEmpty = new NotEmpty();
31+
$passwordNotEmpty->setMessage(__('Password is required field.'), NotEmpty::IS_EMPTY);
32+
$passwordLength = new StringLength(['min' => self::MIN_PASSWORD_LENGTH, 'encoding' => 'UTF-8']);
33+
$passwordLength->setMessage(
34+
__('Your password must be at least %1 characters.', self::MIN_PASSWORD_LENGTH),
35+
StringLength::TOO_SHORT
36+
);
37+
$passwordChars = new Regex('/[a-z].*\d|\d.*[a-z]/iu');
38+
$passwordChars->setMessage(
39+
__('Your password must include both numeric and alphabetic characters.'),
40+
Regex::NOT_MATCH
41+
);
42+
$validator->addRule($passwordNotEmpty,'password');
43+
$validator->addRule($passwordLength, 'password');
44+
$validator->addRule($passwordChars,'password');
45+
46+
return $validator;
47+
}
48+
}

0 commit comments

Comments
 (0)