Skip to content

Commit

Permalink
Add roles via CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
dereuromark committed Dec 28, 2023
1 parent 74900af commit 2fbfa12
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 23 deletions.
18 changes: 14 additions & 4 deletions docs/Authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Example of a record from a valid roles table:
'modified' => '2010-01-07 03:36:33'
```

The `alias` values should be slugged as `lowercase-dashed`.
The `alias` values should be slugged as `lowercase-dashed`.
Multi words like `Super Admin` would be `super-admin` etc.

> Please note that you do NOT need Configure based roles when using database
Expand Down Expand Up @@ -171,7 +171,7 @@ If you need more control over the prefix map, or want to even customize the role
'authorizeByPrefix' => [
'Admin => 'admin',
'Management' => ['mod', 'super-mod'],
'PrefixThree' => ...
'PrefixThree' => ...
],
```

Expand Down Expand Up @@ -242,7 +242,7 @@ view, edit = user
* = * ; All roles can access all actions
```

>**Note:** Prefixes are always `CamelCased`. The route inflects to the final casing if needed.
>**Note:** Prefixes are always `CamelCased`. The route inflects to the final casing if needed.
Nested prefixes are joined using `/`, e.g. `MyAdmin/Nested`.

Using only "granting" is recommended for security reasons.
Expand All @@ -263,7 +263,7 @@ Make sure you are using each section key only once, though. The first definition

### Template with defaults
See the `config/` folder and the default template for popular plugins.
You can copy out any default rules you want to use in your project.
You can copy out any default rules you want to use in your project.

## Adapters
By default INI files and the IniAdapter will be used.
Expand Down Expand Up @@ -400,6 +400,16 @@ This will then add any missing controller with `* = ...` for all actions and you
Note: Use `'*'` as wildcard role if you just want to generate all possible controllers.
Use with `-d -v` to just output the changes it would do to your ACL INI file.

## Add Command

Add any role to any command and action:
```
bin/cake tiny_auth_add {Controller} {Action} {roles, comma separated}
```
It will skip if the roles are already present for this controller and action.

Use with `-d -v` to just output the changes it would do to your ACL INI file.

## Tips

### Use constants instead of magic strings
Expand Down
97 changes: 97 additions & 0 deletions src/Command/TinyAuthAddCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace TinyAuth\Command;

use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use TinyAuth\Sync\Adder;
use TinyAuth\Utility\TinyAuth;

/**
* Auth and ACL helper
*/
class TinyAuthAddCommand extends Command {

/**
* Main function Prints out the list of shells.
*
* @param \Cake\Console\Arguments $args The command arguments.
* @param \Cake\Console\ConsoleIo $io The console io
* @return int
*/
public function execute(Arguments $args, ConsoleIo $io) {
$adder = $this->_getAdder();

$controller = $args->getArgument('controller');
if ($controller === null) {
$controllerNames = $adder->controllers($args);
$io->out('Select a controller:');
foreach ($controllerNames as $controllerName) {
$io->out(' - ' . $controllerName);
}
while (!$controller || !in_array($controller, $controllerNames, true)) {
$controller = $io->ask('Controller name');
}
}

$action = $args->getArgument('action') ?: '*';
$roles = $args->getArgument('roles') ?: '*';
$roles = array_map('trim', explode(',', $roles));
$adder->addAcl($controller, $action, $roles, $args, $io);
$io->out('Controllers and ACL synced.');

return static::CODE_SUCCESS;
}

/**
* @return \TinyAuth\Sync\Adder
*/
protected function _getAdder() {
return new Adder();
}

/**
* Gets the option parser instance and configures it.
*
* @param \Cake\Console\ConsoleOptionParser $parser The parser to build
* @return \Cake\Console\ConsoleOptionParser
*/
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser {
$roles = $this->_getAvailableRoles();

$parser->setDescription(
'Get the list of controllers and make sure, they are synced into the ACL file.',
)->addArgument('controller', [
'help' => 'Controller name (Plugin.Prefix/Name) without Controller suffix.',
'required' => false,
])->addArgument('action', [
'help' => 'Action name (camelCased or under_scored), defaults to `*` (all).',
'required' => false,
])->addArgument('roles', [
'help' => 'Role names, comma separated, e.g. `user,admin`, defaults to `*` (all).' . ($roles ? PHP_EOL . 'Available roles: ' . implode(', ', $roles) . '.' : ''),
'required' => false,
])->addOption('plugin', [
'short' => 'p',
'help' => 'Plugin, use `all` to include all loaded plugins.',
'default' => null,
])->addOption('dry-run', [
'short' => 'd',
'help' => 'Dry Run (only output, do not modify INI files).',
'boolean' => true,
]);

return $parser;
}

/**
* @return array<string>
*/
protected function _getAvailableRoles() {
$roles = (new TinyAuth())->getAvailableRoles();

return array_keys($roles);
}

}
19 changes: 1 addition & 18 deletions src/Command/TinyAuthSyncCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\CommandCollection;
use Cake\Console\CommandCollectionAwareInterface;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use TinyAuth\Sync\Syncer;
Expand All @@ -14,22 +12,7 @@
/**
* Auth and ACL helper
*/
class TinyAuthSyncCommand extends Command implements CommandCollectionAwareInterface {

/**
* The command collection to get help on.
*
* @var \Cake\Console\CommandCollection
*/
protected $commands;

/**
* @param \Cake\Console\CommandCollection $commands The commands to use.
* @return void
*/
public function setCommandCollection(CommandCollection $commands): void {
$this->commands = $commands;
}
class TinyAuthSyncCommand extends Command {

/**
* Main function Prints out the list of shells.
Expand Down
213 changes: 213 additions & 0 deletions src/Sync/Adder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?php

namespace TinyAuth\Sync;

use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Core\Plugin;
use TinyAuth\Filesystem\Folder;
use TinyAuth\Utility\Utility;

class Adder {

/**
* @var array<string, mixed>
*/
protected array $config;

public function __construct() {
$defaults = [
'aclFile' => 'auth_acl.ini',
'aclFilePath' => null,
];
$this->config = (array)Configure::read('TinyAuth') + $defaults;
}

/**
* @var array|null
*/
protected $authAllow;

/**
* @param string $controller
* @param string $action
* @param array<string> $roles
* @param \Cake\Console\Arguments $args
* @param \Cake\Console\ConsoleIo $io
*
* @return void
*/
public function addAcl(string $controller, string $action, array $roles, Arguments $args, ConsoleIo $io) {
$path = $this->config['aclFilePath'] ?: ROOT . DS . 'config' . DS;
$file = $path . $this->config['aclFile'];
$content = Utility::parseFile($file);

if (isset($content[$controller][$action]) || isset($content[$controller]['*'])) {
$mappedRoles = $content[$controller][$action] ?? $content[$controller]['*'];
if (strpos($mappedRoles, ',') !== false) {
$mappedRoles = array_map('trim', explode(',', $mappedRoles));
}
$this->checkRoles($roles, (array)$mappedRoles, $io);
}

$io->info('Add [' . $controller . '] ' . $action . ' = ' . implode(', ', $roles));
$content[$controller][$action] = implode(', ', $roles);

if ($args->getOption('dry-run')) {
$string = Utility::buildIniString($content);

if ($args->getOption('verbose')) {
$io->info('=== ' . $this->config['aclFile'] . ' ===');
$io->info($string);
$io->info('=== ' . $this->config['aclFile'] . ' end ===');
}

return;
}

Utility::generateFile($file, $content);
}

/**
* @param string|null $plugin
* @return array
*/
protected function _getControllers($plugin) {
if ($plugin === 'all') {
$plugins = Plugin::loaded();

$controllers = [];
foreach ($plugins as $plugin) {
$controllers = array_merge($controllers, $this->_getControllers($plugin));
}

return $controllers;
}

$folders = App::classPath('Controller', $plugin);

$controllers = [];
foreach ($folders as $folder) {
$controllers = array_merge($controllers, $this->_parseControllers($folder, $plugin));
}

return $controllers;
}

/**
* @param string $folder Path
* @param string|null $plugin
* @param string|null $prefix
*
* @return array
*/
protected function _parseControllers($folder, $plugin, $prefix = null) {
$folderContent = (new Folder($folder))->read(Folder::SORT_NAME, true);

$controllers = [];
foreach ($folderContent[1] as $file) {
$className = pathinfo($file, PATHINFO_FILENAME);

if (!preg_match('#^(.+)Controller$#', $className, $matches)) {
continue;
}
$name = $matches[1];
if ($matches[1] === 'App') {
continue;
}

if ($this->_noAuthenticationNeeded($name, $plugin, $prefix)) {
continue;
}

$controllers[] = ($plugin ? $plugin . '.' : '') . ($prefix ? $prefix . '/' : '') . $name;
}

foreach ($folderContent[0] as $subFolder) {
$prefixes = (array)Configure::read('TinyAuth.prefixes') ?: null;

if ($prefixes !== null && !in_array($subFolder, $prefixes, true)) {
continue;
}

$controllers = array_merge($controllers, $this->_parseControllers($folder . $subFolder . DS, $plugin, $subFolder));
}

return $controllers;
}

/**
* @param string $name
* @param string|null $plugin
* @param string|null $prefix
* @return bool
*/
protected function _noAuthenticationNeeded($name, $plugin, $prefix) {
if (!isset($this->authAllow)) {
$this->authAllow = $this->_parseAuthAllow();
}

$key = $name;
if (!isset($this->authAllow[$key])) {
return false;
}

if ($this->authAllow[$key] === '*') {
return true;
}

//TODO: specific actions?
return false;
}

/**
* @return array
*/
protected function _parseAuthAllow() {
$defaults = [
'allowFilePath' => null,
'allowFile' => 'auth_allow.ini',
];
$config = (array)Configure::read('TinyAuth') + $defaults;

$path = $config['allowFilePath'] ?: ROOT . DS . 'config' . DS;
$file = $path . $config['allowFile'];

return Utility::parseFile($file);
}

/**
* @param \Cake\Console\Arguments $args
*
* @return array
*/
public function controllers(Arguments $args): array {
//$path = $this->config['aclFilePath'] ?: ROOT . DS . 'config' . DS;
//$file = $path . $this->config['aclFile'];
//$content = Utility::parseFile($file);

$controllers = $this->_getControllers((string)$args->getOption('plugin') ?: null);

return $controllers;
}

/**
* @param array<string> $roles
* @param array<string> $mappedRoles
* @param \Cake\Console\ConsoleIo $io
*
* @return void
*/
protected function checkRoles(array $roles, array $mappedRoles, ConsoleIo $io): void {
foreach ($roles as $role) {
if (!in_array($role, $mappedRoles, true) && !in_array('*', $mappedRoles, true)) {
return;
}
}

$io->abort('Already present. Aborting');
}

}
Loading

0 comments on commit 2fbfa12

Please sign in to comment.