GibbonEdu / core

Gibbon is a flexible, open source school management platform designed to make life better for teachers, students, parents and leaders.
https://gibbonedu.org
GNU General Public License v3.0
462 stars 300 forks source link

Discussion: Access Management #1702

Open SKuipers opened 1 year ago

SKuipers commented 1 year ago

In reviewing #1673 and #1666, we're at a position in the refactoring to make some architectural decisions regarding access management. Currently, almost all user access to a certain page or permission is determined through the functions isActionAccessible() and getHighestGroupedAction(). These functions use a legacy style access to the database and need deprecated, as seen in #1673 and #1666.

Considerations

The solutions in #1673 and #1666 are a great start. @yookoala I like your approach to parsing out the actions in a testable way, and using an AccessManager class to internalize the calls to the gateway. However, in reviewing them together, I have some thoughts on the proposed code changes:

I've put together some ideas on how we might approach these in a unified manner, while also hopefully laying some groundwork for future routing.

Architectural Suggestions

Using a class with static methods does have its drawbacks, however in this case as a core service, like Format, I think it may be worth considering. It would give a nice fluent Access::isAllowed syntax, and could be dropped-in to replace functions without any dependencies (just a use declaration).

Here's my thinking for the Access interface contract. Let me know what you think. Alternatively, getCurrentAction could return an Action object, which exposes the getModule and getActionName methods, which may be more clean.

/**
 * Access class contract for determining if users can perform an action based 
 * on the configured permissions for their role.
 *
 * @version  v25
 * @since    v25
 */
interface Access
{
    public static function initialize(Session $session, ActionGateway $actionGateway);

    public static function isInitialized() : bool;

    public static function getCurrentModule() : string;

    public static function getCurrentAction() : string;

    public static function isAllowed(string $module, string $route, ?string $action = null) : bool;

    public static function getHighestLevel(string $module, string $route) : string;
}

@yookoala Let me know what you think. I'm aiming for something that builds on what you've started in your PRs, and ideally takes us one step further in the overall refactoring in the system, while still being backwards-compatible enough for legacy routes.

yookoala commented 1 year ago

I separate #1673 and #1666 to keep both PR simpler. Also I do not understand the access model of Gibbon enough to also combine getHighestGroupedAction into the access manager. Ideally, everything related to access should be the responsibility of the access manager, not some Gateway class.

Consideration

This is an important change in the framework. And the design matters. I'd prefer to stick to the single responsibility principle so things are more flexible to change later.

On one hand, we need a way to describe a particular action in details (i.e. module, route, additional action name). On the other hand, we need an actor that have knowledge about the current session / user and determine if she / he can do the particular action.

Hence I designed Action and AccessManager to be 2 distinct class.

To combine the 2 into 1 single class would probably force to stick with a particular way to describe action (i.e. isAllowed() sticks to $module, $route and $action) and I think this is not ideal.

Other Development Concerns

There are 1244 files using isActionAccessible(). It's a pain sticking task to do all these files by hand. I use sed and grep to automatically modify most of these files. Then I ran some auto checks for syntax issues. Having isActionAccessible refactor to handle both a route and an Action is simply easier to do than a single step rewrite.

1673 was supposed to be the step 1 of the refactor. Once the change is approved. My next step AccessManager would be to convert all isActionAccessible calls with comparable AccessManager method. Since all the URL path parameters of isActionAccessible are already converted to Action, things should be much easier.

My Idea

To accommodate both isActionAccessible and getHighestGroupedAction change, this is my idea.

isActionAccessible

if (isActionAccessible($guid, $connection2, '/modules/Data Updater/data_updates.php'))) {
    // ... some logics ...
}

into something like:

if ($accessManager->getAccessOf(Action::fromRoute('Data Updater', 'data_updates'))->allow()) {
    // ... some logics ...
}

getHighestGroupedAction

$highestAction = getHighestGroupedAction($guid, '/modules/Activities/activities_attendance.php', $connection2);
if ($highestAction == "Enter Activity Attendance" ||
    ($highestAction == "Enter Activity Attendance_leader" && ($activity['role'] == 'Organiser' || $activity['role'] == 'Assistant' || $activity['role'] == 'Coach'))) {
    // ... some logics ...
}

into something like:

$access = $accessManager->getAccessOf(Action::fromRoute('Activities', 'activities_attendance'));
if ($access->allow('Enter Activity Attendance') ||
    ($access->allow('Enter Activity Attendance_leader') && ($activity['role'] == 'Organiser' || $activity['role'] == 'Assistant' || $activity['role'] == 'Coach'))) {
    // ... some logics ...
}
yookoala commented 1 year ago

Renaming the Action class as Resource might be more sensible. It represents combination of module and route path (i.e. URLList in the gibbonAction) table. The "thingy" that an access "allow" would be the actual "actions".

The logic goes:

The class signatures:


namespace Gibbon\Auth\Access;

class Resource {
  public static function fromRoute(string $module, string $routePath): Resource;
  public function getModule(): string;
  public function getRoutePath(): string;
}

class AccessManager {
  public function getAccessOf(Resource $resource): Access
}

class Access {
  public function allow(?string $action = null): bool;
  public function allowAny(string ...$actions): bool;
  public function allowAll(string ...$actions): bool;
}