From 712eafc36a95d1042d95a3fe93226a8d21b15b8a Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 24 Dec 2021 23:49:05 +0000 Subject: [PATCH 1/2] Update toolkit --- .github/workflows/deduplicate.yml | 14 +++---- .github/workflows/inspect.yml | 4 +- .github/workflows/test.yml | 8 ++-- .github/workflows/unused.yml | 60 ++++++++++++++++++++++++++++++ composer.json | 6 +-- depfile.yaml | 3 ++ phpstan.neon.dist | 2 +- tests/_support/PermitsTestCase.php | 7 +++- 8 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/unused.yml diff --git a/.github/workflows/deduplicate.yml b/.github/workflows/deduplicate.yml index 0b3994c..827bbbc 100644 --- a/.github/workflows/deduplicate.yml +++ b/.github/workflows/deduplicate.yml @@ -7,16 +7,18 @@ on: branches: - 'develop' paths: + - 'app/**' - 'src/**' - 'tests/**' - - '.github/workflows/test-phpcpd.yml' + - '.github/workflows/deduplicate.yml' push: branches: - 'develop' paths: + - 'app/**' - 'src/**' - 'tests/**' - - '.github/workflows/test-phpcpd.yml' + - '.github/workflows/deduplicate.yml' jobs: build: @@ -30,10 +32,8 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.0' - tools: phive - extensions: intl, json, mbstring, xml + tools: phpcpd + extensions: dom, mbstring - name: Detect code duplication - run: | - sudo phive --no-progress install --global --trust-gpg-keys 4AA394086372C20A phpcpd - phpcpd src/ tests/ + run: phpcpd app/ src/ tests/ diff --git a/.github/workflows/inspect.yml b/.github/workflows/inspect.yml index 9ad7ee5..23440fe 100644 --- a/.github/workflows/inspect.yml +++ b/.github/workflows/inspect.yml @@ -75,5 +75,5 @@ jobs: - name: Run architectural inspection run: | - sudo phive --no-progress install --global --trust-gpg-keys B8F640134AB1782E,A98E898BB53EB748 qossmic/deptrac - deptrac analyze --cache-file=build/deptrac.cache + sudo phive --no-progress install --global --trust-gpg-keys B8F640134AB1782E,A98E898BB53EB748 qossmic/deptrac + deptrac analyze --cache-file=build/deptrac.cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 561601d..ce38955 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - tools: composer, pecl, phpunit + tools: composer, infection, pecl, phive, phpunit extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3 coverage: xdebug env: @@ -64,13 +64,15 @@ jobs: - if: matrix.php-versions == '8.0' name: Mutate with Infection run: | - composer global require infection/infection git fetch --depth=1 origin $GITHUB_BASE_REF infection --threads=2 --skip-initial-tests --coverage=build/phpunit --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-filter=AM --logger-github --ignore-msi-with-no-mutations - if: matrix.php-versions == '8.0' name: Run Coveralls - run: vendor/bin/php-coveralls --verbose --coverage_clover=build/phpunit/clover.xml --json_path build/phpunit/coveralls-upload.json + continue-on-error: true + run: | + sudo phive --no-progress install --global --trust-gpg-keys E82B2FB314E9906E php-coveralls + php-coveralls --verbose --coverage_clover=build/phpunit/clover.xml --json_path build/phpunit/coveralls-upload.json env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_PARALLEL: true diff --git a/.github/workflows/unused.yml b/.github/workflows/unused.yml new file mode 100644 index 0000000..4d2ee36 --- /dev/null +++ b/.github/workflows/unused.yml @@ -0,0 +1,60 @@ +# When a PR is opened or a push is made, check code +# for unused packages with Composer Unused. +name: Unused + +on: + pull_request: + branches: + - 'develop' + paths: + - 'src/**' + - 'tests/**' + - '.github/workflows/unused.yml' + push: + branches: + - 'develop' + paths: + - 'src/**' + - 'tests/**' + - '.github/workflows/unused.yml' + +jobs: + build: + name: Unused Package Detection + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + tools: composer, composer-unused + extensions: intl, json, mbstring, xml + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies (limited) + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }} + run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + + - name: Install dependencies (authenticated) + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + env: + COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} + + - name: Detect unused packages + run: composer-unused -vvv --profile --ansi --no-interaction --no-progress --excludePackage=php diff --git a/composer.json b/composer.json index 77455d9..71aa48e 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,7 @@ }, "require-dev": { "codeigniter4/codeigniter4": "dev-develop", - "codeigniter4/codeigniter4-standard": "^1.0", - "tatter/tools": "^1.12" + "tatter/tools": "^1.15" }, "autoload": { "psr-4": { @@ -43,7 +42,7 @@ "repositories": [ { "type": "vcs", - "url": "https://github.com/codeigniter4/CodeIgniter4" + "url": "https://github.com/codeigniter4/CodeIgniter4.git" } ], "minimum-stability": "dev", @@ -55,7 +54,6 @@ "@deduplicate", "@analyze", "@test", - "@mutate", "@inspect", "@style" ], diff --git a/depfile.yaml b/depfile.yaml index 79ec82a..4ca1cc7 100644 --- a/depfile.yaml +++ b/depfile.yaml @@ -109,6 +109,7 @@ ruleset: - Entity - Service - Vendor Config + - Vendor Entity - Vendor Model Service: - Config @@ -116,6 +117,7 @@ ruleset: # Ignore anything in the Vendor layers Vendor Model: + - Config - Service - Vendor Config - Vendor Controller @@ -130,6 +132,7 @@ ruleset: - Vendor Model - Vendor View Vendor Config: + - Config - Service - Vendor Config - Vendor Controller diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4ae2fa8..3bfd4d7 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,7 +6,7 @@ parameters: - tests bootstrapFiles: - vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php - excludes_analyse: + excludePaths: - src/Config/Routes.php - src/Views/* - src/Models/UserModel.php diff --git a/tests/_support/PermitsTestCase.php b/tests/_support/PermitsTestCase.php index b634c5c..3c1212e 100644 --- a/tests/_support/PermitsTestCase.php +++ b/tests/_support/PermitsTestCase.php @@ -2,11 +2,16 @@ namespace Tests\Support; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; + /** * @internal */ -abstract class PermitsTestCase extends \CodeIgniter\Test\CIDatabaseTestCase +abstract class PermitsTestCase extends CIUnitTestCase { + use DatabaseTestTrait; + /** * Should the database be refreshed before each test? * From 61119171f1b4d21d57aad0d53de76b43ccef999b Mon Sep 17 00:00:00 2001 From: MGatner Date: Fri, 24 Dec 2021 23:49:35 +0000 Subject: [PATCH 2/2] Apply updated coding style --- .php-cs-fixer.dist.php | 16 +- examples/Permits.php | 18 +- src/Commands/PermitsAdd.php | 102 ++-- src/Commands/PermitsList.php | 88 ++-- src/Config/Permits.php | 16 +- src/Config/Services.php | 17 +- ...019-03-26-110032_create_permits_tables.php | 44 +- src/Exceptions/PermitsException.php | 34 +- src/Filters/PermitsFilter.php | 92 ++-- src/Helpers/chmod_helper.php | 83 ++-- src/Interfaces/PermitsUserModelInterface.php | 16 +- src/Language/en/Permits.php | 8 +- src/Model.php | 45 +- src/Models/PermitModel.php | 32 +- src/Models/UserModel.php | 60 ++- src/Permits.php | 446 +++++++++--------- src/Traits/PermitsTrait.php | 407 ++++++++-------- .../2019-09-02-092335_create_test_tables.php | 104 ++-- .../_support/Database/Seeds/PermitSeeder.php | 267 +++++------ tests/_support/Models/FactoryModel.php | 48 +- tests/_support/PermitsTestCase.php | 66 +-- tests/database/DatabaseTest.php | 26 +- tests/unit/HelperTest.php | 44 +- tests/unit/ServiceTest.php | 70 +-- tests/unit/TraitTest.php | 42 +- 25 files changed, 1038 insertions(+), 1153 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 29c0518..dfa45af 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -10,12 +10,7 @@ ->exclude('build') ->append([__FILE__]); -// Remove overrides for incremental changes -$overrides = [ - 'array_indentation' => false, - 'braces' => false, - 'indentation_type' => false, -]; +$overrides = []; $options = [ 'finder' => $finder, @@ -23,12 +18,3 @@ ]; return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); - -/* Reenable For libraries after incremental changes are applied -return Factory::create(new CodeIgniter4(), $overrides, $options)->forLibrary( - 'Tatter ________', - 'Tatter Software', - '', - 2021 -); - */ diff --git a/examples/Permits.php b/examples/Permits.php index 13acc0e..3500693 100644 --- a/examples/Permits.php +++ b/examples/Permits.php @@ -14,16 +14,16 @@ class Permits extends \Tatter\Permits\Config\Permits { - // key in $_SESSION that contains the integer ID of a logged in user - public $sessionUserId = 'logged_in'; + // key in $_SESSION that contains the integer ID of a logged in user + public $sessionUserId = 'logged_in'; - // whether to implement groups access across the library - // set to 'false' if you don't have a groups table implemented - public $useGroups = true; + // whether to implement groups access across the library + // set to 'false' if you don't have a groups table implemented + public $useGroups = true; - // number of seconds to cache a permission - public $cacheDuration = 60; + // number of seconds to cache a permission + public $cacheDuration = 60; - // whether to continue instead of throwing exceptions - public $silent = true; + // whether to continue instead of throwing exceptions + public $silent = true; } diff --git a/src/Commands/PermitsAdd.php b/src/Commands/PermitsAdd.php index 68933a8..2df5f19 100644 --- a/src/Commands/PermitsAdd.php +++ b/src/Commands/PermitsAdd.php @@ -8,59 +8,51 @@ class PermitsAdd extends BaseCommand { - protected $group = 'Auth'; - protected $name = 'permits:add'; - protected $description = 'Adds a permit to the database.'; - - protected $usage = 'permits:add [permission] [target] [id]'; - protected $arguments = [ - 'permission' => "The name of the permission to grant (e.g. 'listJobs')", - 'target' => "The type of recipient ('groups' or 'users')", - 'id' => "The ID of the recipient (e.g. '42')", - ]; - - public function run(array $params = []) - { - $permits = new PermitModel(); - - // Consume or prompt for the permission name - $permission = array_shift($params); - if (empty($permission)) - { - $permission = CLI::prompt('Permission to grant', null, 'required'); - } - - // Consume or prompt for the target table - $target = array_shift($params); - if (empty($target)) - { - $target = CLI::prompt('Target', ['groups', 'users']); - } - - // Consume or prompt for the target ID - $id = array_shift($params); - if (empty($id)) - { - $id = CLI::prompt(ucfirst(substr($target, 0, -1)) . ' ID', null, 'is_natural_no_zero'); - } - - if ($target === 'groups') - { - $row['group_id'] = $id; - } - else { - $row['user_id'] = $id; - } - $row['name'] = $permission; - - try { - $permits->save($row); - } - catch (\Exception $e) - { - $this->showError($e); - } - - $this->call('permits:list'); - } + protected $group = 'Auth'; + protected $name = 'permits:add'; + protected $description = 'Adds a permit to the database.'; + protected $usage = 'permits:add [permission] [target] [id]'; + protected $arguments = [ + 'permission' => "The name of the permission to grant (e.g. 'listJobs')", + 'target' => "The type of recipient ('groups' or 'users')", + 'id' => "The ID of the recipient (e.g. '42')", + ]; + + public function run(array $params = []) + { + $permits = new PermitModel(); + + // Consume or prompt for the permission name + $permission = array_shift($params); + if (empty($permission)) { + $permission = CLI::prompt('Permission to grant', null, 'required'); + } + + // Consume or prompt for the target table + $target = array_shift($params); + if (empty($target)) { + $target = CLI::prompt('Target', ['groups', 'users']); + } + + // Consume or prompt for the target ID + $id = array_shift($params); + if (empty($id)) { + $id = CLI::prompt(ucfirst(substr($target, 0, -1)) . ' ID', null, 'is_natural_no_zero'); + } + + if ($target === 'groups') { + $row['group_id'] = $id; + } else { + $row['user_id'] = $id; + } + $row['name'] = $permission; + + try { + $permits->save($row); + } catch (\Exception $e) { + $this->showError($e); + } + + $this->call('permits:list'); + } } diff --git a/src/Commands/PermitsList.php b/src/Commands/PermitsList.php index 237f775..05a4886 100644 --- a/src/Commands/PermitsList.php +++ b/src/Commands/PermitsList.php @@ -7,50 +7,46 @@ class PermitsList extends BaseCommand { - protected $group = 'Auth'; - protected $name = 'permits:list'; - protected $description = 'Lists permits assigned explicitly in the database.'; - - public function run(array $params) - { - $db = db_connect(); - - // User permits - CLI::write(' USER PERMITS ', 'white', 'black'); - - // get all user permits - $rows = $db->table('permits')->select('user_id, name, created_by, created_at') - ->where('user_id >', 0) - ->orderBy('user_id', 'asc') - ->orderBy('name', 'asc') - ->get()->getResultArray(); - - if (empty($rows)) - { - CLI::write(CLI::color('No user permits granted.', 'yellow')); - } - else { - $thead = ['User ID', 'Permission', 'Granted By', 'Granted Date']; - CLI::table($rows, $thead); - } - - // Group permits - CLI::write(' GROUP PERMITS ', 'white', 'black'); - - // get all user permits - $rows = $db->table('permits')->select('group_id, name, created_by, created_at') - ->where('group_id >', 0) - ->orderBy('group_id', 'asc') - ->orderBy('name', 'asc') - ->get()->getResultArray(); - - if (empty($rows)) - { - CLI::write(CLI::color('No group permits granted.', 'yellow')); - } - else { - $thead = ['Group ID', 'Permission', 'Granted By', 'Granted Date']; - CLI::table($rows, $thead); - } - } + protected $group = 'Auth'; + protected $name = 'permits:list'; + protected $description = 'Lists permits assigned explicitly in the database.'; + + public function run(array $params) + { + $db = db_connect(); + + // User permits + CLI::write(' USER PERMITS ', 'white', 'black'); + + // get all user permits + $rows = $db->table('permits')->select('user_id, name, created_by, created_at') + ->where('user_id >', 0) + ->orderBy('user_id', 'asc') + ->orderBy('name', 'asc') + ->get()->getResultArray(); + + if (empty($rows)) { + CLI::write(CLI::color('No user permits granted.', 'yellow')); + } else { + $thead = ['User ID', 'Permission', 'Granted By', 'Granted Date']; + CLI::table($rows, $thead); + } + + // Group permits + CLI::write(' GROUP PERMITS ', 'white', 'black'); + + // get all user permits + $rows = $db->table('permits')->select('group_id, name, created_by, created_at') + ->where('group_id >', 0) + ->orderBy('group_id', 'asc') + ->orderBy('name', 'asc') + ->get()->getResultArray(); + + if (empty($rows)) { + CLI::write(CLI::color('No group permits granted.', 'yellow')); + } else { + $thead = ['Group ID', 'Permission', 'Granted By', 'Granted Date']; + CLI::table($rows, $thead); + } + } } diff --git a/src/Config/Permits.php b/src/Config/Permits.php index d4a787b..876984e 100644 --- a/src/Config/Permits.php +++ b/src/Config/Permits.php @@ -6,15 +6,15 @@ class Permits extends BaseConfig { - // key in $_SESSION that contains the integer ID of a logged in user - public $sessionUserId = 'logged_in'; + // key in $_SESSION that contains the integer ID of a logged in user + public $sessionUserId = 'logged_in'; - // whether to implement groups access across the library - public $useGroups = true; + // whether to implement groups access across the library + public $useGroups = true; - // number of seconds to cache a permission - public $cacheDuration = 60; + // number of seconds to cache a permission + public $cacheDuration = 60; - // whether to continue instead of throwing exceptions - public $silent = true; + // whether to continue instead of throwing exceptions + public $silent = true; } diff --git a/src/Config/Services.php b/src/Config/Services.php index 527054f..e70ec8b 100644 --- a/src/Config/Services.php +++ b/src/Config/Services.php @@ -9,15 +9,14 @@ class Services extends BaseService { - public static function permits(?PermitsConfig $config = null, ?PermitsUserModelInterface $userModel = null, bool $getShared = true) - { - if ($getShared) - { - return static::getSharedInstance('permits', $config, $userModel); - } + public static function permits(?PermitsConfig $config = null, ?PermitsUserModelInterface $userModel = null, bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('permits', $config, $userModel); + } - $config = $config ?? config('Permits'); + $config = $config ?? config('Permits'); - return new Permits($config, $userModel); - } + return new Permits($config, $userModel); + } } diff --git a/src/Database/Migrations/2019-03-26-110032_create_permits_tables.php b/src/Database/Migrations/2019-03-26-110032_create_permits_tables.php index b9a201b..5330765 100644 --- a/src/Database/Migrations/2019-03-26-110032_create_permits_tables.php +++ b/src/Database/Migrations/2019-03-26-110032_create_permits_tables.php @@ -6,30 +6,30 @@ class CreatePermitsTables extends Migration { - public function up() - { - $fields = [ - 'name' => ['type' => 'VARCHAR', 'constraint' => 63], - 'user_id' => ['type' => 'INT', 'null' => true], - 'group_id' => ['type' => 'INT', 'null' => true], - 'created_by' => ['type' => 'INT', 'null' => true], - 'created_at' => ['type' => 'DATETIME', 'null' => true], - 'updated_at' => ['type' => 'DATETIME', 'null' => true], - ]; + public function up() + { + $fields = [ + 'name' => ['type' => 'VARCHAR', 'constraint' => 63], + 'user_id' => ['type' => 'INT', 'null' => true], + 'group_id' => ['type' => 'INT', 'null' => true], + 'created_by' => ['type' => 'INT', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + ]; - $this->forge->addField('id'); - $this->forge->addField($fields); + $this->forge->addField('id'); + $this->forge->addField($fields); - $this->forge->addKey('name'); - $this->forge->addKey(['user_id', 'name']); - $this->forge->addKey(['group_id', 'name']); - $this->forge->addKey('created_at'); + $this->forge->addKey('name'); + $this->forge->addKey(['user_id', 'name']); + $this->forge->addKey(['group_id', 'name']); + $this->forge->addKey('created_at'); - $this->forge->createTable('permits'); - } + $this->forge->createTable('permits'); + } - public function down() - { - $this->forge->dropTable('permits'); - } + public function down() + { + $this->forge->dropTable('permits'); + } } diff --git a/src/Exceptions/PermitsException.php b/src/Exceptions/PermitsException.php index 0c533f8..3cb47d0 100644 --- a/src/Exceptions/PermitsException.php +++ b/src/Exceptions/PermitsException.php @@ -6,24 +6,24 @@ class PermitsException extends \RuntimeException implements ExceptionInterface { - public static function forMissingDatabaseTable(string $table) - { - return new static(lang('Permits.missingDatabaseTable', [$table])); - } + public static function forMissingDatabaseTable(string $table) + { + return new static(lang('Permits.missingDatabaseTable', [$table])); + } - public static function forInvalidModeType(string $table) - { - return new static(lang('Permits.invalidModelType', [$table])); - } + public static function forInvalidModeType(string $table) + { + return new static(lang('Permits.invalidModelType', [$table])); + } - public static function forInvalidMode(string $table, string $mode) - { - return new static(lang('Permits.invalidMode', [$table, $mode])); - } + public static function forInvalidMode(string $table, string $mode) + { + return new static(lang('Permits.invalidMode', [$table, $mode])); + } - // Generic 'not allowed' exception - public static function forNotPermitted() - { - return new static(lang('Permits.notPermitted')); - } + // Generic 'not allowed' exception + public static function forNotPermitted() + { + return new static(lang('Permits.notPermitted')); + } } diff --git a/src/Filters/PermitsFilter.php b/src/Filters/PermitsFilter.php index de894a7..cabf8f3 100644 --- a/src/Filters/PermitsFilter.php +++ b/src/Filters/PermitsFilter.php @@ -10,63 +10,37 @@ class PermitsFilter implements FilterInterface { - /** - * Do whatever processing this filter needs to do. - * By default it should not return anything during - * normal execution. However, when an abnormal state - * is found, it should return an instance of - * CodeIgniter\HTTP\Response. If it does, script - * execution will end and that Response will be - * sent back to the client, allowing for error pages, - * redirects, etc. - * - * @param null $arguments - * - * @throws PermitsException - * - * @return RedirectResponse|void - */ - public function before(RequestInterface $request, $arguments = null) - { - if (empty($arguments)) - { - return; - } - $permits = service('permits'); - - if (! $userId = $permits->sessionUserId()) - { - return; - } - - // Check each requested permission - foreach ($arguments as $permission) - { - if (! $permits->hasPermit($userId, $permission)) - { - if (config('Permits')->silent) - { - return redirect()->back()->with('error', lang('Permits.notPermitted')); - } - - throw PermitsException::forNotPermitted(); - - } - } - - } - - /** - * Allows After filters to inspect and modify the response - * object as needed. This method does not allow any way - * to stop execution of other after filters, short of - * throwing an Exception or Error. - * - * @param null $arguments - * - * @return mixed - */ - public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) - { - } + /** + * @param array|null $arguments + * + * @throws PermitsException + * + * @return RedirectResponse|void + */ + public function before(RequestInterface $request, $arguments = null) + { + if (empty($arguments)) { + return; + } + $permits = service('permits'); + + if (! $userId = $permits->sessionUserId()) { + return; + } + + // Check each requested permission + foreach ($arguments as $permission) { + if (! $permits->hasPermit($userId, $permission)) { + if (config('Permits')->silent) { + return redirect()->back()->with('error', lang('Permits.notPermitted')); + } + + throw PermitsException::forNotPermitted(); + } + } + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + } } diff --git a/src/Helpers/chmod_helper.php b/src/Helpers/chmod_helper.php index b68c9b6..28dffde 100644 --- a/src/Helpers/chmod_helper.php +++ b/src/Helpers/chmod_helper.php @@ -2,49 +2,44 @@ // https://caboodle.tech/blog/21/06/2017/trusting-user-input-in-phps-chmod-decimal-vs-octal/ -if (! function_exists('octal2array')) -{ - // Parses a perceived octal mode into an array of permissions - function mode2array($mode) - { - if (! is_octal($mode)) - { - return false; - } - - $permissions['domain']['read'] = (bool) ($mode & 04000); - $permissions['domain']['write'] = (bool) ($mode & 02000); - $permissions['domain']['execute'] = (bool) ($mode & 01000); - - $permissions['user']['read'] = (bool) ($mode & 00400); - $permissions['user']['write'] = (bool) ($mode & 00200); - $permissions['user']['execute'] = (bool) ($mode & 00100); - - $permissions['group']['read'] = (bool) ($mode & 00040); - $permissions['group']['write'] = (bool) ($mode & 00020); - $permissions['group']['execute'] = (bool) ($mode & 00010); - - $permissions['world']['read'] = (bool) ($mode & 00004); - $permissions['world']['write'] = (bool) ($mode & 00002); - $permissions['world']['execute'] = (bool) ($mode & 00001); - - return $permissions; - } +if (! function_exists('octal2array')) { + // Parses a perceived octal mode into an array of permissions + function mode2array($mode) + { + if (! is_octal($mode)) { + return false; + } + + $permissions['domain']['read'] = (bool) ($mode & 04000); + $permissions['domain']['write'] = (bool) ($mode & 02000); + $permissions['domain']['execute'] = (bool) ($mode & 01000); + + $permissions['user']['read'] = (bool) ($mode & 00400); + $permissions['user']['write'] = (bool) ($mode & 00200); + $permissions['user']['execute'] = (bool) ($mode & 00100); + + $permissions['group']['read'] = (bool) ($mode & 00040); + $permissions['group']['write'] = (bool) ($mode & 00020); + $permissions['group']['execute'] = (bool) ($mode & 00010); + + $permissions['world']['read'] = (bool) ($mode & 00004); + $permissions['world']['write'] = (bool) ($mode & 00002); + $permissions['world']['execute'] = (bool) ($mode & 00001); + + return $permissions; + } } -if (! function_exists('is_octal')) -{ - // Convert a perceived octal mode to a decimal and then back to check if it really is an octal - function is_octal($octal): bool - { - if (! is_int($octal)) - { - return false; - } - if ($octal < 0 || $octal > 4095) - { - return false; - } - - return octdec(decoct($octal)) === $octal; - } +if (! function_exists('is_octal')) { + // Convert a perceived octal mode to a decimal and then back to check if it really is an octal + function is_octal($octal): bool + { + if (! is_int($octal)) { + return false; + } + if ($octal < 0 || $octal > 4095) { + return false; + } + + return octdec(decoct($octal)) === $octal; + } } diff --git a/src/Interfaces/PermitsUserModelInterface.php b/src/Interfaces/PermitsUserModelInterface.php index 4e74505..1631d00 100644 --- a/src/Interfaces/PermitsUserModelInterface.php +++ b/src/Interfaces/PermitsUserModelInterface.php @@ -4,12 +4,12 @@ interface PermitsUserModelInterface { - /** - * Returns groups for a single user. - * - * @param mixed $userId = null - * - * @return array Usually Group Entities - */ - public function groups($userId = null): array; + /** + * Returns groups for a single user. + * + * @param mixed $userId = null + * + * @return array Usually Group Entities + */ + public function groups($userId = null): array; } diff --git a/src/Language/en/Permits.php b/src/Language/en/Permits.php index efaf2b9..77e5519 100644 --- a/src/Language/en/Permits.php +++ b/src/Language/en/Permits.php @@ -1,8 +1,8 @@ 'Table "{0}" missing for permissions handling."', - 'invalidModelType' => 'Invalid permit mode type on model for "{0}"', - 'invalidMode' => 'Invalid permit mode on model for "{0}": {1}', - 'notPermitted' => 'You do not have permission to do that.', + 'missingDatabaseTable' => 'Table "{0}" missing for permissions handling."', + 'invalidModelType' => 'Invalid permit mode type on model for "{0}"', + 'invalidMode' => 'Invalid permit mode on model for "{0}": {1}', + 'notPermitted' => 'You do not have permission to do that.', ]; diff --git a/src/Model.php b/src/Model.php index 724952c..3b1ba26 100644 --- a/src/Model.php +++ b/src/Model.php @@ -6,25 +6,28 @@ class Model extends \CodeIgniter\Model { - use PermitsTrait; - - /* Default mode: - * 4 Domain list, no create - * 6 Owner read, write - * 6 Group read, write - * 4 World read, no write - */ - protected $mode = 04664; - - // Name of the user ID in this model's objects - protected $userKey; - // Name of the group ID in this model's objects - protected $groupKey; - // Name of this object's ID in the pivot tables - protected $pivotKey; - - // Table that joins this model's objects to its users - protected $usersPivot; - // Table that joins this model's objects to its groups - protected $groupsPivot; + use PermitsTrait; + + /* Default mode: + * 4 Domain list, no create + * 6 Owner read, write + * 6 Group read, write + * 4 World read, no write + */ + protected $mode = 04664; + + // Name of the user ID in this model's objects + protected $userKey; + + // Name of the group ID in this model's objects + protected $groupKey; + + // Name of this object's ID in the pivot tables + protected $pivotKey; + + // Table that joins this model's objects to its users + protected $usersPivot; + + // Table that joins this model's objects to its groups + protected $groupsPivot; } diff --git a/src/Models/PermitModel.php b/src/Models/PermitModel.php index c0ff287..2797601 100644 --- a/src/Models/PermitModel.php +++ b/src/Models/PermitModel.php @@ -6,22 +6,18 @@ class PermitModel extends Model { - protected $table = 'permits'; - protected $primaryKey = 'id'; - - protected $returnType = 'object'; - protected $useSoftDeletes = false; - - protected $allowedFields = [ - 'name', - 'user_id', - 'group_id', - 'created_by', - ]; - - protected $useTimestamps = true; - - protected $validationRules = []; - protected $validationMessages = []; - protected $skipValidation = false; + protected $table = 'permits'; + protected $primaryKey = 'id'; + protected $returnType = 'object'; + protected $useSoftDeletes = false; + protected $allowedFields = [ + 'name', + 'user_id', + 'group_id', + 'created_by', + ]; + protected $useTimestamps = true; + protected $validationRules = []; + protected $validationMessages = []; + protected $skipValidation = false; } diff --git a/src/Models/UserModel.php b/src/Models/UserModel.php index c8f629c..a7270b3 100644 --- a/src/Models/UserModel.php +++ b/src/Models/UserModel.php @@ -16,37 +16,35 @@ */ class UserModel extends Model implements PermitsUserModelInterface { - protected $table = 'users'; - protected $primaryKey = 'id'; - protected $returnType = 'object'; + protected $table = 'users'; + protected $primaryKey = 'id'; + protected $returnType = 'object'; + protected $useTimestamps = false; + protected $useSoftDeletes = false; + protected $skipValidation = true; + protected $allowedFields = []; - protected $useTimestamps = false; - protected $useSoftDeletes = false; - protected $skipValidation = true; + // Permits + protected $mode = 00640; + protected $pivotKey = 'user_id'; + protected $groupsPivot = 'auth_groups_users'; - protected $allowedFields = []; - - // Permits - protected $mode = 00640; - protected $pivotKey = 'user_id'; - protected $groupsPivot = 'auth_groups_users'; - - /** - * Returns groups for a single user. - * - * @see https://github.com/lonnieezell/myth-auth/blob/develop/src/Authorization/GroupModel.php - * - * @param mixed $userId = null - * - * @return array Array of objects (usually Group Entities) - */ - public function groups($userId = null): array - { - return $this->builder() - ->select('auth_groups.id') - ->join($this->groupsPivot, "{$this->groupsPivot}.{$this->pivotKey} = {$this->table}.{$this->primaryKey}", 'left') - ->join('auth_groups', "{$this->groupsPivot}.group_id = auth_groups.id", 'left') - ->where("{$this->groupsPivot}.{$this->pivotKey}", $userId) - ->get()->getResultObject(); - } + /** + * Returns groups for a single user. + * + * @see https://github.com/lonnieezell/myth-auth/blob/develop/src/Authorization/GroupModel.php + * + * @param mixed $userId = null + * + * @return array Array of objects (usually Group Entities) + */ + public function groups($userId = null): array + { + return $this->builder() + ->select('auth_groups.id') + ->join($this->groupsPivot, "{$this->groupsPivot}.{$this->pivotKey} = {$this->table}.{$this->primaryKey}", 'left') + ->join('auth_groups', "{$this->groupsPivot}.group_id = auth_groups.id", 'left') + ->where("{$this->groupsPivot}.{$this->pivotKey}", $userId) + ->get()->getResultObject(); + } } diff --git a/src/Permits.php b/src/Permits.php index 0407440..6adc096 100644 --- a/src/Permits.php +++ b/src/Permits.php @@ -9,239 +9,215 @@ class Permits { - /** - * Our configuration instance. - * - * @var PermitsConfig - */ - protected $config; - - /** - * The permit model used to fetch permits. - * - * @var PermitModel - */ - protected $permitModel; - - /** - * External model to handle users - * - * @var PermitsUserModelInterface - */ - protected $userModel; - - /** - * Initializes the library. - */ - public function __construct(PermitsConfig $config, ?PermitsUserModelInterface $userModel = null) - { - $this->config = $config; - - // Load the models - $this->userModel = $userModel ?? model(UserModel::class); - $this->permitModel = model(PermitModel::class); - - // Load the helper for mode conversions - helper('chmod'); - } - - /** - * Checks for a logged in user based on the configured key. - * - * @return int The user ID, 0 for "not logged in", -1 for CLI - */ - public function sessionUserId(): int - { - if (ENVIRONMENT !== 'testing' && is_cli()) - { - return -1; - } - - return session($this->config->sessionUserId) ?? 0; - } - - // try to cache a permit and pass it back - protected function cache($key, $permit) - { - if ($duration = $this->config->cacheDuration) - { - cache()->save($key, $permit, $duration); - } - - return $permit; - } - - // series fo checks to ensure input is a valid object and model has permissions setup - public function isPermissible($object, $objectModel): bool - { - if (! is_octal($objectModel->mode)) - { - return false; - } - - return ! (empty($object)); - } - - // checks if user is a member of the supplied group - public function userHasGroup(int $userId, int $groupId): ?bool - { - if (! $this->config->useGroups) - { - return null; - } - - foreach ($this->userModel->groups($userId) as $group) - { - if ($groupId === $group->id) - { - return true; - } - } - - return false; - } - - // checks if user is one of an object's owners - public function userHasOwnership(int $userId, $object, $objectModel): bool - { - // make sure the model has the necessary info - if (empty($objectModel->userKey)) - { - return false; - } - - // if input is an array, convert it - if (gettype($object) === 'array') - { - $object = (object) $object; - } - - // check if the object itself has $userKey set - if ($object->{$objectModel->userKey}) - { - return $userId === $object->{$objectModel->userKey}; - - // otherwise, check for a valid pivot table - } - if (! empty($objectModel->usersPivot)) - { - // @phpstan-ignore-next-line - return (bool) $objectModel->db->table($objectModel->usersPivot)->where($objectModel->userKey, $userId)->where($objectModel->pivotKey, $object->{$objectModel->primaryKey}) - ->get()->getResult(); - } - - return false; - } - - // checks if user is a member of an object's groups - public function userHasGroupOwnership(int $userId, $object, $objectModel): ?bool - { - if (! $this->config->useGroups) - { - return null; - } - - // if input is an array, convert it - if (gettype($object) === 'array') - { - $object = (object) $object; - } - - // make sure the model has the necessary info - if (empty($objectModel->groupKey)) - { - return false; - } - - // check if the object itself has $groupKey set - if ($groupId = $object->{$objectModel->groupKey}) - { - // check if this is a group the user is a part of - return $this->userHasGroup($userId, $groupId); - - // otherwise, check for a valid pivot table - } - if (! empty($objectModel->groupsPivot)) - { - // @phpstan-ignore-next-line - return (bool) $objectModel->db->table($objectModel->groupsPivot)->where($objectModel->groupKey, $userId)->where($objectModel->pivotKey, $object->{$objectModel->primaryKey}) // @phpstan-ignore-line - ->get()->getResult(); - } - - return false; - } - - // check if user has direct permit or inherited group permit - public function hasPermit(?int $userId, string $name): bool - { - if (empty($userId)) - { - return false; - } - - // check for cached version - $cacheKey = "permits-{$name}-{$userId}"; - $permit = cache($cacheKey); - if ($permit !== null) - { - return ! empty($this->cache($cacheKey, $permit)); - } - - // check database for user permit - if ($permit = $this->hasUserPermit($userId, $name)) - { - return ! empty($this->cache($cacheKey, $permit)); - } - - if (! $this->config->useGroups) - { - return false; - } - - // check database for each of user's groups - foreach ($this->userModel->groups($userId) as $group) - { - if ($permit = $this->hasGroupPermit($group->id, $name)) - { - return $this->cache($cacheKey, $permit); - } - } - - return false; - } - - // checks for global permit for one user, ignoring groups - public function hasUserPermit(int $userId, string $name): ?bool - { - if (empty($userId)) - { - return null; - } - - // @phpstan-ignore-next-line - return (bool) $this->permitModel - ->where('user_id', $userId) - ->where('name', $name) - ->first(); - } - - // checks for global permit for one group - public function hasGroupPermit(int $groupId, string $name): ?bool - { - if (! $this->config->useGroups) - { - return null; - } - if (empty($groupId)) - { - return null; - } - - // @phpstan-ignore-next-line - return ! empty($this->permitModel - ->where('group_id', $groupId) - ->where('name', $name) - ->first() - ); - } + /** + * Our configuration instance. + * + * @var PermitsConfig + */ + protected $config; + + /** + * The permit model used to fetch permits. + * + * @var PermitModel + */ + protected $permitModel; + + /** + * External model to handle users + * + * @var PermitsUserModelInterface + */ + protected $userModel; + + /** + * Initializes the library. + */ + public function __construct(PermitsConfig $config, ?PermitsUserModelInterface $userModel = null) + { + $this->config = $config; + + // Load the models + $this->userModel = $userModel ?? model(UserModel::class); + $this->permitModel = model(PermitModel::class); // @phpstan-ignore-line + + // Load the helper for mode conversions + helper('chmod'); + } + + /** + * Checks for a logged in user based on the configured key. + * + * @return int The user ID, 0 for "not logged in", -1 for CLI + */ + public function sessionUserId(): int + { + if (ENVIRONMENT !== 'testing' && is_cli()) { + return -1; + } + + return session($this->config->sessionUserId) ?? 0; + } + + // try to cache a permit and pass it back + protected function cache($key, $permit) + { + if ($duration = $this->config->cacheDuration) { + cache()->save($key, $permit, $duration); + } + + return $permit; + } + + // series fo checks to ensure input is a valid object and model has permissions setup + public function isPermissible($object, $objectModel): bool + { + if (! is_octal($objectModel->mode)) { + return false; + } + + return ! (empty($object)); + } + + // checks if user is a member of the supplied group + public function userHasGroup(int $userId, int $groupId): ?bool + { + if (! $this->config->useGroups) { + return null; + } + + foreach ($this->userModel->groups($userId) as $group) { + if ($groupId === $group->id) { + return true; + } + } + + return false; + } + + // checks if user is one of an object's owners + public function userHasOwnership(int $userId, $object, $objectModel): bool + { + // make sure the model has the necessary info + if (empty($objectModel->userKey)) { + return false; + } + + // if input is an array, convert it + if (gettype($object) === 'array') { + $object = (object) $object; + } + + // check if the object itself has $userKey set + if ($object->{$objectModel->userKey}) { + return $userId === $object->{$objectModel->userKey}; + + // otherwise, check for a valid pivot table + } + if (! empty($objectModel->usersPivot)) { + // @phpstan-ignore-next-line + return (bool) $objectModel->db->table($objectModel->usersPivot)->where($objectModel->userKey, $userId)->where($objectModel->pivotKey, $object->{$objectModel->primaryKey}) + ->get()->getResult(); + } + + return false; + } + + // checks if user is a member of an object's groups + public function userHasGroupOwnership(int $userId, $object, $objectModel): ?bool + { + if (! $this->config->useGroups) { + return null; + } + + // if input is an array, convert it + if (gettype($object) === 'array') { + $object = (object) $object; + } + + // make sure the model has the necessary info + if (empty($objectModel->groupKey)) { + return false; + } + + // check if the object itself has $groupKey set + if ($groupId = $object->{$objectModel->groupKey}) { + // check if this is a group the user is a part of + return $this->userHasGroup($userId, $groupId); + + // otherwise, check for a valid pivot table + } + if (! empty($objectModel->groupsPivot)) { + // @phpstan-ignore-next-line + return (bool) $objectModel->db->table($objectModel->groupsPivot)->where($objectModel->groupKey, $userId)->where($objectModel->pivotKey, $object->{$objectModel->primaryKey}) // @phpstan-ignore-line + ->get()->getResult(); + } + + return false; + } + + // check if user has direct permit or inherited group permit + public function hasPermit(?int $userId, string $name): bool + { + if (empty($userId)) { + return false; + } + + // check for cached version + $cacheKey = "permits-{$name}-{$userId}"; + $permit = cache($cacheKey); + if ($permit !== null) { + return ! empty($this->cache($cacheKey, $permit)); + } + + // check database for user permit + if ($permit = $this->hasUserPermit($userId, $name)) { + return ! empty($this->cache($cacheKey, $permit)); + } + + if (! $this->config->useGroups) { + return false; + } + + // check database for each of user's groups + foreach ($this->userModel->groups($userId) as $group) { + if ($permit = $this->hasGroupPermit($group->id, $name)) { + return $this->cache($cacheKey, $permit); + } + } + + return false; + } + + // checks for global permit for one user, ignoring groups + public function hasUserPermit(int $userId, string $name): ?bool + { + if (empty($userId)) { + return null; + } + + // @phpstan-ignore-next-line + return (bool) $this->permitModel + ->where('user_id', $userId) + ->where('name', $name) + ->first(); + } + + // checks for global permit for one group + public function hasGroupPermit(int $groupId, string $name): ?bool + { + if (! $this->config->useGroups) { + return null; + } + if (empty($groupId)) { + return null; + } + + // @phpstan-ignore-next-line + return ! empty($this->permitModel + ->where('group_id', $groupId) + ->where('name', $name) + ->first() + ); + } } diff --git a/src/Traits/PermitsTrait.php b/src/Traits/PermitsTrait.php index 97dd26a..2bb0025 100644 --- a/src/Traits/PermitsTrait.php +++ b/src/Traits/PermitsTrait.php @@ -6,219 +6,198 @@ trait PermitsTrait { - // Whether the current/supplied user may insert rows into this model's table - public function mayCreate(?int $userId = null): bool - { - // Check for admin permit - if ($this->mayAdmin($userId)) - { - return true; - } - - // Load the library and check for a user - $permits = service('permits'); - $userId = $userId ?? $permits->sessionUserId(); - - // Check for a permit - if ($permit = $permits->hasPermit($userId, 'create' . ucfirst($this->table))) - { - return true; - } - - // Make sure the mode is setup correctly - if (! is_octal($this->mode)) - { - return false; - } - - // Verify the mode - if (! $permissions = mode2array($this->mode)) - { - return false; - } - - // Check for domain writeable (create) - if ($permissions['domain']['write']) - { - return true; - } - - // If logged in then check for user writable - return (bool) ($userId && $permissions['user']['write']); - } - - // Whether the current/supplied user may read the given object - public function mayRead($object, ?int $userId = null): bool - { - // Check for admin permit - if ($this->mayAdmin($userId)) - { - return true; - } - - // Load the library and check for a user - $permits = service('permits'); - $userId = $userId ?? $permits->sessionUserId(); - - // Check for an explicit permit - if ($permit = $permits->hasPermit($userId, 'read' . ucfirst($this->table))) - { - return true; - } - - // Make sure permissions are setup correctly - if (! $permits->isPermissible($object, $this)) - { - return false; - } - $permissions = mode2array($this->mode); - - // Check if the object is world-readable - if ($permissions['world']['read']) - { - return true; - } - - // Check if the object is group-readable - if ($permissions['group']['read'] && $permits->userHasGroupOwnership($userId, $object, $this)) - { - return true; - } - - // Check if the object is user-readable - return (bool) ($permissions['user']['read'] && $permits->userHasOwnership($userId, $object, $this)); - } - - // Whether the current/supplied user may update the given object - public function mayUpdate($object, ?int $userId = null): bool - { - // Check for admin permit - if ($this->mayAdmin($userId)) - { - return true; - } - - // Load the library and check for a user - $permits = service('permits'); - $userId = $userId ?? $permits->sessionUserId(); - - // Check for a permit - if ($permit = $permits->hasPermit($userId, 'update' . ucfirst($this->table))) - { - return true; - } - - // Make sure permissions are setup correctly - if (! $permits->isPermissible($object, $this)) - { - return false; - } - $permissions = mode2array($this->mode); - - // Check if the object is world-writeable - if ($permissions['world']['write']) - { - return true; - } - - // Check if the object is group-writeable - if ($permissions['group']['write'] && $permits->userHasGroupOwnership($userId, $object, $this)) - { - return true; - } - - // Check if the object is user-writeable - return (bool) ($permissions['user']['write'] && $permits->userHasOwnership($userId, $object, $this)); - } - - // Whether the current/supplied user may delete the given object - public function mayDelete($object, ?int $userId = null): bool - { - // Check for admin permit - if ($this->mayAdmin($userId)) - { - return true; - } - - return $this->mayUpdate($object, $userId); - } - - // Whether the current/supplied user may list rows from this model's table - public function mayList(?int $userId = null): bool - { - // Check for admin permit - if ($this->mayAdmin($userId)) - { - return true; - } - - // Load the library and check for a user - $permits = service('permits'); - $userId = $userId ?? $permits->sessionUserId(); - - // Check for a permit - if ($permit = $permits->hasPermit($userId, 'list' . ucfirst($this->table))) - { - return true; - } - - // Make sure permissions are setup correctly - if (! is_octal($this->mode)) - { - return false; - } - - // Check if the domain is readable - if ($permissions = mode2array($this->mode)) - { - return $permissions['domain']['read']; - } - - return false; - } - - // Whether the current/supplied user may perform any of the other actions - public function mayAdmin(?int $userId = null): bool - { - // Load the library and check for a user - $permits = service('permits'); - $userId = $userId ?? $permits->sessionUserId(); - - // Check for the permit - return (bool) ($permit = $permits->hasPermit($userId, 'admin' . ucfirst($this->table))) - - // Deny all other requests + // Whether the current/supplied user may insert rows into this model's table + public function mayCreate(?int $userId = null): bool + { + // Check for admin permit + if ($this->mayAdmin($userId)) { + return true; + } + + // Load the library and check for a user + $permits = service('permits'); + $userId = $userId ?? $permits->sessionUserId(); + + // Check for a permit + if ($permit = $permits->hasPermit($userId, 'create' . ucfirst($this->table))) { + return true; + } + + // Make sure the mode is setup correctly + if (! is_octal($this->mode)) { + return false; + } + + // Verify the mode + if (! $permissions = mode2array($this->mode)) { + return false; + } + + // Check for domain writeable (create) + if ($permissions['domain']['write']) { + return true; + } + + // If logged in then check for user writable + return (bool) ($userId && $permissions['user']['write']); + } + + // Whether the current/supplied user may read the given object + public function mayRead($object, ?int $userId = null): bool + { + // Check for admin permit + if ($this->mayAdmin($userId)) { + return true; + } + + // Load the library and check for a user + $permits = service('permits'); + $userId = $userId ?? $permits->sessionUserId(); + + // Check for an explicit permit + if ($permit = $permits->hasPermit($userId, 'read' . ucfirst($this->table))) { + return true; + } + + // Make sure permissions are setup correctly + if (! $permits->isPermissible($object, $this)) { + return false; + } + $permissions = mode2array($this->mode); + + // Check if the object is world-readable + if ($permissions['world']['read']) { + return true; + } + + // Check if the object is group-readable + if ($permissions['group']['read'] && $permits->userHasGroupOwnership($userId, $object, $this)) { + return true; + } + + // Check if the object is user-readable + return (bool) ($permissions['user']['read'] && $permits->userHasOwnership($userId, $object, $this)); + } + + // Whether the current/supplied user may update the given object + public function mayUpdate($object, ?int $userId = null): bool + { + // Check for admin permit + if ($this->mayAdmin($userId)) { + return true; + } + + // Load the library and check for a user + $permits = service('permits'); + $userId = $userId ?? $permits->sessionUserId(); + + // Check for a permit + if ($permit = $permits->hasPermit($userId, 'update' . ucfirst($this->table))) { + return true; + } + + // Make sure permissions are setup correctly + if (! $permits->isPermissible($object, $this)) { + return false; + } + $permissions = mode2array($this->mode); + + // Check if the object is world-writeable + if ($permissions['world']['write']) { + return true; + } + + // Check if the object is group-writeable + if ($permissions['group']['write'] && $permits->userHasGroupOwnership($userId, $object, $this)) { + return true; + } + + // Check if the object is user-writeable + return (bool) ($permissions['user']['write'] && $permits->userHasOwnership($userId, $object, $this)); + } + + // Whether the current/supplied user may delete the given object + public function mayDelete($object, ?int $userId = null): bool + { + // Check for admin permit + if ($this->mayAdmin($userId)) { + return true; + } + + return $this->mayUpdate($object, $userId); + } + + // Whether the current/supplied user may list rows from this model's table + public function mayList(?int $userId = null): bool + { + // Check for admin permit + if ($this->mayAdmin($userId)) { + return true; + } + + // Load the library and check for a user + $permits = service('permits'); + $userId = $userId ?? $permits->sessionUserId(); + + // Check for a permit + if ($permit = $permits->hasPermit($userId, 'list' . ucfirst($this->table))) { + return true; + } + + // Make sure permissions are setup correctly + if (! is_octal($this->mode)) { + return false; + } + + // Check if the domain is readable + if ($permissions = mode2array($this->mode)) { + return $permissions['domain']['read']; + } + + return false; + } + + // Whether the current/supplied user may perform any of the other actions + public function mayAdmin(?int $userId = null): bool + { + // Load the library and check for a user + $permits = service('permits'); + $userId = $userId ?? $permits->sessionUserId(); + + // Check for the permit + return (bool) ($permit = $permits->hasPermit($userId, 'admin' . ucfirst($this->table))) + + // Deny all other requests ; - } - - //-------------------------------------------------------------------- - - /** - * Changes the access mode. - * - * @param int $mode Integer representation of octal mode. Default 04664 - * - * @return $this - */ - public function setMode(int $mode): self - { - helper('chmod'); - if (! is_octal($mode)) - { - throw new PermitsException($this->table, $mode); - } - $this->mode = $mode; - - return $this; - } - - /** - * Returns the access mode. - * - * @return int Integer representation of octal mode. Default 04664 - */ - public function getMode(): int - { - return $this->mode; - } + } + + //-------------------------------------------------------------------- + + /** + * Changes the access mode. + * + * @param int $mode Integer representation of octal mode. Default 04664 + * + * @return $this + */ + public function setMode(int $mode): self + { + helper('chmod'); + if (! is_octal($mode)) { + throw new PermitsException($this->table, $mode); + } + $this->mode = $mode; + + return $this; + } + + /** + * Returns the access mode. + * + * @return int Integer representation of octal mode. Default 04664 + */ + public function getMode(): int + { + return $this->mode; + } } diff --git a/tests/_support/Database/Migrations/2019-09-02-092335_create_test_tables.php b/tests/_support/Database/Migrations/2019-09-02-092335_create_test_tables.php index 09ee4c4..b3cda2c 100644 --- a/tests/_support/Database/Migrations/2019-09-02-092335_create_test_tables.php +++ b/tests/_support/Database/Migrations/2019-09-02-092335_create_test_tables.php @@ -6,44 +6,44 @@ class CreateTestTables extends Migration { - public function up() - { - // Factories - $fields = [ - 'group_id' => ['type' => 'int', 'null' => true], - 'name' => ['type' => 'varchar', 'constraint' => 31], - 'uid' => ['type' => 'varchar', 'constraint' => 31], - 'class' => ['type' => 'varchar', 'constraint' => 63], - 'icon' => ['type' => 'varchar', 'constraint' => 31], - 'summary' => ['type' => 'varchar', 'constraint' => 255], - 'created_at' => ['type' => 'datetime', 'null' => true], - 'updated_at' => ['type' => 'datetime', 'null' => true], - 'deleted_at' => ['type' => 'datetime', 'null' => true], - ]; + public function up() + { + // Factories + $fields = [ + 'group_id' => ['type' => 'int', 'null' => true], + 'name' => ['type' => 'varchar', 'constraint' => 31], + 'uid' => ['type' => 'varchar', 'constraint' => 31], + 'class' => ['type' => 'varchar', 'constraint' => 63], + 'icon' => ['type' => 'varchar', 'constraint' => 31], + 'summary' => ['type' => 'varchar', 'constraint' => 255], + 'created_at' => ['type' => 'datetime', 'null' => true], + 'updated_at' => ['type' => 'datetime', 'null' => true], + 'deleted_at' => ['type' => 'datetime', 'null' => true], + ]; - $this->forge->addField('id'); - $this->forge->addField($fields); + $this->forge->addField('id'); + $this->forge->addField($fields); - $this->forge->addKey('name'); - $this->forge->addKey('uid'); - $this->forge->addKey(['deleted_at', 'id']); - $this->forge->addKey('created_at'); + $this->forge->addKey('name'); + $this->forge->addKey('uid'); + $this->forge->addKey(['deleted_at', 'id']); + $this->forge->addKey('created_at'); - $this->forge->createTable('factories'); + $this->forge->createTable('factories'); - // Factories-Users - $fields = [ + // Factories-Users + $fields = [ 'factory_id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'default' => 0], 'user_id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'default' => 0], ]; - $this->forge->addField($fields); - $this->forge->addKey(['factory_id', 'user_id']); - $this->forge->createTable('factories_users', true); + $this->forge->addField($fields); + $this->forge->addKey(['factory_id', 'user_id']); + $this->forge->createTable('factories_users', true); - // Test Auth tables modified from https://github.com/lonnieezell/myth-auth + // Test Auth tables modified from https://github.com/lonnieezell/myth-auth - // Users - $this->forge->addField([ + // Users + $this->forge->addField([ 'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 'email' => ['type' => 'varchar', 'constraint' => 255], 'username' => ['type' => 'varchar', 'constraint' => 30, 'null' => true], @@ -61,37 +61,37 @@ public function up() 'updated_at' => ['type' => 'datetime', 'null' => true], 'deleted_at' => ['type' => 'datetime', 'null' => true], ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey('email'); - $this->forge->addUniqueKey('username'); - $this->forge->createTable('users', true); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('email'); + $this->forge->addUniqueKey('username'); + $this->forge->createTable('users', true); - // Groups - $fields = [ + // Groups + $fields = [ 'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 'name' => ['type' => 'varchar', 'constraint' => 255], 'description' => ['type' => 'varchar', 'constraint' => 255], ]; - $this->forge->addField($fields); - $this->forge->addKey('id', true); - $this->forge->createTable('auth_groups', true); + $this->forge->addField($fields); + $this->forge->addKey('id', true); + $this->forge->createTable('auth_groups', true); - // Groups-Users - $fields = [ + // Groups-Users + $fields = [ 'group_id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'default' => 0], 'user_id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'default' => 0], ]; - $this->forge->addField($fields); - $this->forge->addKey(['group_id', 'user_id']); - $this->forge->createTable('auth_groups_users', true); - } + $this->forge->addField($fields); + $this->forge->addKey(['group_id', 'user_id']); + $this->forge->createTable('auth_groups_users', true); + } - public function down() - { - $this->forge->dropTable('factories'); - $this->forge->dropTable('factories_users'); - $this->forge->dropTable('users'); - $this->forge->dropTable('auth_groups'); - $this->forge->dropTable('auth_groups_users'); - } + public function down() + { + $this->forge->dropTable('factories'); + $this->forge->dropTable('factories_users'); + $this->forge->dropTable('users'); + $this->forge->dropTable('auth_groups'); + $this->forge->dropTable('auth_groups_users'); + } } diff --git a/tests/_support/Database/Seeds/PermitSeeder.php b/tests/_support/Database/Seeds/PermitSeeder.php index 28edc4f..7b5785c 100644 --- a/tests/_support/Database/Seeds/PermitSeeder.php +++ b/tests/_support/Database/Seeds/PermitSeeder.php @@ -4,140 +4,135 @@ class PermitSeeder extends \CodeIgniter\Database\Seeder { - public function run() - { - // Test Auth seeds modified from https://github.com/lonnieezell/myth-auth - - // USERS - $users = [ - [ - 'email' => 'yamira@noted.com', - 'username' => 'light', - 'password_hash' => password_hash('secretK33P3R', PASSWORD_DEFAULT), - ], - [ - 'email' => 'kazuto.kirigaya@castle.org', - 'username' => 'kirito', - 'password_hash' => password_hash('swordsX2', PASSWORD_DEFAULT), - ], - [ - 'email' => 'Mittelman@example.com', - 'username' => 'Saitama', - 'password_hash' => password_hash('1punch', PASSWORD_DEFAULT), - ], - ]; - - $builder = $this->db->table('users'); - - foreach ($users as $user) - { - $builder->insert($user); - } - - // GROUPS - $groups = [ - [ - 'name' => 'Administrators', - 'description' => 'Users with ultimate power', - ], - [ - 'name' => 'Blacklisted', - 'description' => 'Users sequestered for misconduct', - ], - [ - 'name' => 'Puny', - 'description' => 'Users who can do next to nothing', - ], - ]; - - $builder = $this->db->table('auth_groups'); - - foreach ($groups as $group) - { - $builder->insert($group); - } - - // GROUPS-USERS - $rows = [ - [ - 'group_id' => 1, - 'user_id' => 1, - ], - [ - 'group_id' => 2, - 'user_id' => 1, - ], - [ - 'group_id' => 3, - 'user_id' => 2, - ], - ]; - - $builder = $this->db->table('auth_groups_users'); - - foreach ($rows as $row) - { - $builder->insert($row); - } - - // Industrial seeds - - // FACTORIES - $factories = [ - [ - 'group_id' => 1, - 'name' => 'Test Factory', - 'uid' => 'test001', - 'class' => 'Factories\Tests\NewFactory', - 'icon' => 'fas fa-puzzle-piece', - 'summary' => 'Longer sample text for testing', - ], - [ - 'group_id' => null, - 'name' => 'Widget Factory', - 'uid' => 'widget', - 'class' => 'Factories\Tests\WidgetPlant', - 'icon' => 'fas fa-puzzle-piece', - 'summary' => 'Create widgets in your factory', - ], - [ - 'group_id' => 2, - 'name' => 'Evil Factory', - 'uid' => 'evil-maker', - 'class' => 'Factories\Evil\MyFactory', - 'icon' => 'fas fa-book-dead', - 'summary' => 'Abandon all hope, ye who enter here', - ], - ]; - - $builder = $this->db->table('factories'); - - foreach ($factories as $factory) - { - $builder->insert($factory); - } - - // FACTORIES-USERS - $rows = [ - [ - 'factory_id' => 1, - 'user_id' => 1, - ], - [ - 'factory_id' => 2, - 'user_id' => 1, - ], - [ - 'factory_id' => 3, - 'user_id' => 2, - ], - ]; - - $builder = $this->db->table('factories_users'); - - foreach ($rows as $row) - { - $builder->insert($row); - } - } + public function run() + { + // Test Auth seeds modified from https://github.com/lonnieezell/myth-auth + + // USERS + $users = [ + [ + 'email' => 'yamira@noted.com', + 'username' => 'light', + 'password_hash' => password_hash('secretK33P3R', PASSWORD_DEFAULT), + ], + [ + 'email' => 'kazuto.kirigaya@castle.org', + 'username' => 'kirito', + 'password_hash' => password_hash('swordsX2', PASSWORD_DEFAULT), + ], + [ + 'email' => 'Mittelman@example.com', + 'username' => 'Saitama', + 'password_hash' => password_hash('1punch', PASSWORD_DEFAULT), + ], + ]; + + $builder = $this->db->table('users'); + + foreach ($users as $user) { + $builder->insert($user); + } + + // GROUPS + $groups = [ + [ + 'name' => 'Administrators', + 'description' => 'Users with ultimate power', + ], + [ + 'name' => 'Blacklisted', + 'description' => 'Users sequestered for misconduct', + ], + [ + 'name' => 'Puny', + 'description' => 'Users who can do next to nothing', + ], + ]; + + $builder = $this->db->table('auth_groups'); + + foreach ($groups as $group) { + $builder->insert($group); + } + + // GROUPS-USERS + $rows = [ + [ + 'group_id' => 1, + 'user_id' => 1, + ], + [ + 'group_id' => 2, + 'user_id' => 1, + ], + [ + 'group_id' => 3, + 'user_id' => 2, + ], + ]; + + $builder = $this->db->table('auth_groups_users'); + + foreach ($rows as $row) { + $builder->insert($row); + } + + // Industrial seeds + + // FACTORIES + $factories = [ + [ + 'group_id' => 1, + 'name' => 'Test Factory', + 'uid' => 'test001', + 'class' => 'Factories\Tests\NewFactory', + 'icon' => 'fas fa-puzzle-piece', + 'summary' => 'Longer sample text for testing', + ], + [ + 'group_id' => null, + 'name' => 'Widget Factory', + 'uid' => 'widget', + 'class' => 'Factories\Tests\WidgetPlant', + 'icon' => 'fas fa-puzzle-piece', + 'summary' => 'Create widgets in your factory', + ], + [ + 'group_id' => 2, + 'name' => 'Evil Factory', + 'uid' => 'evil-maker', + 'class' => 'Factories\Evil\MyFactory', + 'icon' => 'fas fa-book-dead', + 'summary' => 'Abandon all hope, ye who enter here', + ], + ]; + + $builder = $this->db->table('factories'); + + foreach ($factories as $factory) { + $builder->insert($factory); + } + + // FACTORIES-USERS + $rows = [ + [ + 'factory_id' => 1, + 'user_id' => 1, + ], + [ + 'factory_id' => 2, + 'user_id' => 1, + ], + [ + 'factory_id' => 3, + 'user_id' => 2, + ], + ]; + + $builder = $this->db->table('factories_users'); + + foreach ($rows as $row) { + $builder->insert($row); + } + } } diff --git a/tests/_support/Models/FactoryModel.php b/tests/_support/Models/FactoryModel.php index d5c71d0..77018a1 100644 --- a/tests/_support/Models/FactoryModel.php +++ b/tests/_support/Models/FactoryModel.php @@ -6,30 +6,26 @@ class FactoryModel extends Model { - protected $table = 'factories'; - protected $primaryKey = 'id'; - - protected $returnType = 'object'; - protected $useSoftDeletes = false; - - protected $allowedFields = [ - 'group_id', - 'name', - 'uid', - 'class', - 'icon', - 'summary', - ]; - - protected $useTimestamps = true; - - protected $validationRules = []; - protected $validationMessages = []; - protected $skipValidation = false; - - // Permits - public $mode = 04660; - public $groupKey = 'group_id'; - public $pivotKey = 'factory_id'; - public $usersPivot = 'factories_users'; + protected $table = 'factories'; + protected $primaryKey = 'id'; + protected $returnType = 'object'; + protected $useSoftDeletes = false; + protected $allowedFields = [ + 'group_id', + 'name', + 'uid', + 'class', + 'icon', + 'summary', + ]; + protected $useTimestamps = true; + protected $validationRules = []; + protected $validationMessages = []; + protected $skipValidation = false; + + // Permits + public $mode = 04660; + public $groupKey = 'group_id'; + public $pivotKey = 'factory_id'; + public $usersPivot = 'factories_users'; } diff --git a/tests/_support/PermitsTestCase.php b/tests/_support/PermitsTestCase.php index 3c1212e..3198d0e 100644 --- a/tests/_support/PermitsTestCase.php +++ b/tests/_support/PermitsTestCase.php @@ -10,41 +10,41 @@ */ abstract class PermitsTestCase extends CIUnitTestCase { - use DatabaseTestTrait; + use DatabaseTestTrait; - /** - * Should the database be refreshed before each test? - * - * @var bool - */ - protected $refresh = true; + /** + * Should the database be refreshed before each test? + * + * @var bool + */ + protected $refresh = true; - /** - * The seed file(s) used for all tests within this test case. - * Should be fully-namespaced or relative to $basePath - * - * @var array|string - */ - protected $seed = 'Tests\Support\Database\Seeds\PermitSeeder'; + /** + * The seed file(s) used for all tests within this test case. + * Should be fully-namespaced or relative to $basePath + * + * @var array|string + */ + protected $seed = 'Tests\Support\Database\Seeds\PermitSeeder'; - /** - * The path to the seeds directory. - * Allows overriding the default application directories. - * - * @var string - */ - protected $basePath = SUPPORTPATH . 'Database/'; + /** + * The path to the seeds directory. + * Allows overriding the default application directories. + * + * @var string + */ + protected $basePath = SUPPORTPATH . 'Database/'; - /** - * The namespace(s) to help us find the migration classes. - * Empty is equivalent to running `spark migrate -all`. - * Note that running "all" runs migrations in date order, - * but specifying namespaces runs them in namespace order (then date) - * - * @var array|string|null - */ - protected $namespace = [ - 'Tests\Support', - 'Tatter\Permits', - ]; + /** + * The namespace(s) to help us find the migration classes. + * Empty is equivalent to running `spark migrate -all`. + * Note that running "all" runs migrations in date order, + * but specifying namespaces runs them in namespace order (then date) + * + * @var array|string|null + */ + protected $namespace = [ + 'Tests\Support', + 'Tatter\Permits', + ]; } diff --git a/tests/database/DatabaseTest.php b/tests/database/DatabaseTest.php index a7712cc..3f28e37 100644 --- a/tests/database/DatabaseTest.php +++ b/tests/database/DatabaseTest.php @@ -7,22 +7,22 @@ */ final class DatabaseTest extends \Tests\Support\PermitsTestCase { - public function testMayList() - { - session()->logged_in = 2; + public function testMayList() + { + session()->logged_in = 2; - $model = new FactoryModel(); + $model = new FactoryModel(); - $this->assertTrue($model->mayList()); - } + $this->assertTrue($model->mayList()); + } - public function testMayCreate() - { - session()->logged_in = 2; + public function testMayCreate() + { + session()->logged_in = 2; - $model = new FactoryModel(); - $model->mode = 02640; + $model = new FactoryModel(); + $model->mode = 02640; - $this->assertTrue($model->mayCreate()); - } + $this->assertTrue($model->mayCreate()); + } } diff --git a/tests/unit/HelperTest.php b/tests/unit/HelperTest.php index a42612f..4ecaec3 100644 --- a/tests/unit/HelperTest.php +++ b/tests/unit/HelperTest.php @@ -5,29 +5,29 @@ */ final class HelperTest extends \CodeIgniter\Test\CIUnitTestCase { - protected function setUp(): void - { - parent::setUp(); - helper('chmod'); - } + protected function setUp(): void + { + parent::setUp(); + helper('chmod'); + } - public function testIsOctalTrue() - { - $this->assertTrue(is_octal(0001)); - $this->assertTrue(is_octal(0604)); - $this->assertTrue(is_octal(0777)); - } + public function testIsOctalTrue() + { + $this->assertTrue(is_octal(0001)); + $this->assertTrue(is_octal(0604)); + $this->assertTrue(is_octal(0777)); + } - public function testIsOctalFalse() - { - $this->assertFalse(is_octal('0001')); - $this->assertFalse(is_octal(7777)); - } + public function testIsOctalFalse() + { + $this->assertFalse(is_octal('0001')); + $this->assertFalse(is_octal(7777)); + } - public function testMode2Array() - { - $array = mode2array(0755); - $this->assertFalse($array['world']['write']); - $this->assertTrue($array['user']['execute']); - } + public function testMode2Array() + { + $array = mode2array(0755); + $this->assertFalse($array['world']['write']); + $this->assertTrue($array['user']['execute']); + } } diff --git a/tests/unit/ServiceTest.php b/tests/unit/ServiceTest.php index 0f34130..b47177c 100644 --- a/tests/unit/ServiceTest.php +++ b/tests/unit/ServiceTest.php @@ -7,52 +7,52 @@ */ final class ServiceTest extends \CodeIgniter\Test\CIUnitTestCase { - // Instance of our service - protected $permits; + // Instance of our service + protected $permits; - protected function setUp(): void - { - parent::setUp(); + protected function setUp(): void + { + parent::setUp(); - $this->permits = service('Permits'); - } + $this->permits = service('Permits'); + } - public function testIsPermissibleTrue() - { - $object = new \stdClass(); - $object->name = 'foobar'; + public function testIsPermissibleTrue() + { + $object = new \stdClass(); + $object->name = 'foobar'; - $model = new FactoryModel(); + $model = new FactoryModel(); - $this->assertTrue($this->permits->isPermissible($object, $model)); - } + $this->assertTrue($this->permits->isPermissible($object, $model)); + } - public function testIsPermissibleFalseWithoutObject() - { - $model = new FactoryModel(); + public function testIsPermissibleFalseWithoutObject() + { + $model = new FactoryModel(); - $this->assertFalse($this->permits->isPermissible(null, $model)); - } + $this->assertFalse($this->permits->isPermissible(null, $model)); + } - public function testIsPermissibleFalseWithInvalidMode() - { - $object = new \stdClass(); - $object->name = 'foobar'; + public function testIsPermissibleFalseWithInvalidMode() + { + $object = new \stdClass(); + $object->name = 'foobar'; - $model = new FactoryModel(); - $model->mode = 024644; + $model = new FactoryModel(); + $model->mode = 024644; - $this->assertFalse($this->permits->isPermissible($object, $model)); - } + $this->assertFalse($this->permits->isPermissible($object, $model)); + } - public function testIsPermissibleFalseWithStringMode() - { - $object = new \stdClass(); - $object->name = 'foobar'; + public function testIsPermissibleFalseWithStringMode() + { + $object = new \stdClass(); + $object->name = 'foobar'; - $model = new FactoryModel(); - $model->mode = '4644'; + $model = new FactoryModel(); + $model->mode = '4644'; - $this->assertFalse($this->permits->isPermissible($object, $model)); - } + $this->assertFalse($this->permits->isPermissible($object, $model)); + } } diff --git a/tests/unit/TraitTest.php b/tests/unit/TraitTest.php index eb93d0f..df5c4ae 100644 --- a/tests/unit/TraitTest.php +++ b/tests/unit/TraitTest.php @@ -8,33 +8,33 @@ */ final class TraitTest extends CIUnitTestCase { - /** - * @var FactoryModel - */ - protected $model; + /** + * @var FactoryModel + */ + protected $model; - protected function setUp(): void - { - parent::setUp(); + protected function setUp(): void + { + parent::setUp(); - $this->model = new FactoryModel(); - } + $this->model = new FactoryModel(); + } - public function testGetMode() - { - $result = $this->model->getMode(); + public function testGetMode() + { + $result = $this->model->getMode(); - $this->assertSame(04660, $result); - } + $this->assertSame(04660, $result); + } - public function testSetMode() - { - $mode = 06600; + public function testSetMode() + { + $mode = 06600; - $this->model->setMode($mode); + $this->model->setMode($mode); - $result = $this->model->getMode(); + $result = $this->model->getMode(); - $this->assertSame($mode, $result); - } + $this->assertSame($mode, $result); + } }