renoki-co/laravel-acl

Simple, AWS IAM-style ACL for Laravel applications, leveraging granular permissions in your applications with strong declarations.

dev-master 2022-12-16 07:23 UTC

README

CI codecov StyleCI Latest Stable Version Total Downloads Monthly Downloads License

Simple, AWS IAM-style ACL for Laravel applications, leveraging granular permissions in your applications with strong declarations. 🔐

🚀 Installation

You can install the package via composer:

composer require renoki-co/laravel-acl

Publish the config:

php artisan vendor:publish --provider="RenokiCo\LaravelAcl\LaravelAclServiceProvider" --tag="config"

Publish the migrations:

php artisan vendor:publish --provider="RenokiCo\LaravelAcl\LaravelAclServiceProvider" --tag="migrations"

🙌 Usage

This package is based on renoki-co/acl, but leveraging the Laravel environment to quickly set up permissions.

In case you are familiar with how ARNs and Policies work, you can now use the same syntax to define and check your ACL policies.

You can check more IAM examples to get a sense how to define your policies.

The role of an ACL system is to assign policies or rules/statements to specific entities that can perform certain actions on a set of resources, so later you can verify them throughout the app.

In Laravel, the actor class is most of the times the authenticatable model. To set up the actor class, you have to trait it up with HasPolicies:

use RenokiCo\LaravelAcl\Concerns\HasPolicies;
use RenokiCo\LaravelAcl\Contracts\RuledByPolicies;
// ...

class User extends Authenticatable implements RuledByPolicies
{
    use HasPolicies;

    // ...
}

Whenever you require the actor to check for permissions, the package will automatically pull the attached permissions. To attach a policy, simply define a static AclPolicy and attach it directly to the actor, with a custom name (that can be helpful to identify a specific attachment):

use RenokiCo\Acl\Acl;
use RenokiCo\Acl\Statement;

$policy = Acl::createPolicy([
    Statement::make(
        effect: 'Allow',
        action: 'server:List',
        resource: [
            'arn:php:default:local:123:server',
        ],
    ),
]);

$user = Auth::user();

// Do this once, as it stores the policy in the database, in relation to the user.
$user->attachPolicy('ListServers', $policy);

$user->isAllowedTo(
    'server:List',
    'arn:php:default:local:123:server',
); // true

🧬 ARNables

This documentation part might be outdated and was tweaked to represent this specific use case. Consider reading the ACL documentation, taking into account this current guide.

PHP is more object-oriented. ARNables can help turn your models or classes into a simpler version of ARNs, so you don't have to write all your ARNs each time, but instead pass them to the isAllowedTo() method, depending on either it's an ARN that is resource-agnostic, or an ARN that points to a specific resource.

Resource-agnostic ARN vs Resource ARN

Resource-agnostic ARNs are the ones that are used for actions like list or create. They are not pointing to a specific resource, but rather to a "general" permission for that resource, that can lead to allowing listing or creating resources. For example, arn:php:default:local:123:server.

Resource ARNs are the ARNs that point to a specific resource. Actions like delete, modify and such are good examples that can be used in combination with these ARNs. For example, arn:php:default:local:123:server/1 or arn:php:default:local:123:backup/1.

Resolving the Region and Account IDs

Let's take this ARN example: arn:php:default:local:123:server.

Since this ARN is agnostic, the Server class cannot be properly converted to an ARN without two key components:

  • the region, in this case local
  • the account ID, in this case 123

Although the values do have defaults, you must let the ACL service know what the values should be.

For these values, you can take AWS' example: it lets you select the region (in console: by manually changing the region via the top-right selector; in the API: by specifying the --region parameter), and you must be authenticated to an account, in this case your current login session knows your Account ID.

Resolving within Models

Any actor that has the HasPolicies trait alredy resolves the region as local and the account ID as the primary key. The best approach is to override the region resolver based on the current request session, for example:

class User extends Authenticatable implements RuledByPolicies
{
    use HasPolicies;

    /**
     * Resolve the account ID of the current actor.
     * This value will be used in ARNs for ARNable static instances,
     * to see if the current actor can perform ID-agnostic resource actions.
     *
     * @return null|string|int
     */
    public function resolveArnAccountId()
    {
        return $this->getKey();
    }

    /**
     * Resolve the region of the current actor.
     * This value will be used in ARNs for ARNable static instances,
     * to see if the current actor can perform ID-agnostic resource actions.
     *
     * @return null|string|int
     */
    public function resolveArnRegion()
    {
        return session('region', 'local');
    }
}

// i.e. Handle region change via HTTP requests
public function changeRegion(Request $request)
{
    $data = $request->validate([
        'region' => ['required', 'string', 'in:us-east-1,eu-central-1'],
    ]);

    session(['region' => $data['region']]);

    return redirect('/');
}

Using ARNables with actors

Let's say you have a model called Server instance that belongs to an user:

use RenokiCo\LaravelAcl\Concerns\HasArn;
use RenokiCo\LaravelAcl\Contracts\Arnable;
use RenokiCo\LaravelAcl\BuildResourceArn;

class Server extends Model implements Arnable
{
    use HasArn;

    protected $fillable = [
        'user_id',
        'name',
        'ip',
    ];

    public function arnResourceAccountId()
    {
        return $this->user_id;
    }
}

Instead of passing full ARNs to ->isAllowedTo, you can now pass the server class name instead:

$policy = Acl::createPolicy([
    Statement::make(
        effect: 'Allow',
        action: 'server:List',
        resource: [
            'arn:php:default:local:123:server',
        ],
    ),
    Statement::make(
        effect: 'Allow',
        action: 'server:Delete',
        resource: [
            'arn:php:default:local:123:server/1',
        ],
    ),
]);

$user = Auth::user();

$user->isAllowedTo('server:List', Server::class); // true

To check permissions on a specific resource ARN, you may pass the object itself to the ARN parameter:

$server = Server::find('1');
$user->isAllowedTo('server:Delete', $server); // true

As you have seen previously, on the actor model you can specify the account identifier for them. In an ARN like arn:php:default:local:123:server, the part 123 is the account ID, or the account identifier. Thus, setting resolveArnAccountId to return 123, the policies will allow the actor to server:List on that specific resource.

The resource ID gets resolved automatically to the model primary key, but you can override it:

class Server extends Model implements Arnable
{
    use HasArn;

    public function arnResourceId()
    {
        return $this->getKey();
    }
}

Using ARNables with teams

On a more complex note, having a model that groups more actors, like a Team having more Users is pretty common, especially if using Laravel Jetstream.

You still need to implement the policy checking at the user level, but with regard to resolving the "account ID" to be more like Team ID, as long as the resources are created under Team.

class Team extends Model
{
    //
}
use RenokiCo\LaravelAcl\Concerns\HasPolicies;
use RenokiCo\LaravelAcl\Contracts\RuledByPolicies;

class User extends Authenticatable implements RuledByPolicies
{
    use HasPolicies;

    public function resolveArnAccountId()
    {
        return $this->current_team_id;
    }
}

Im the ARNables' case, their models should resolve the account ID of the team:

class Server extends Model implements Arnable
{
    use HasArn;

    public function arnResourceAccountId()
    {
        return $this->team_id;
    }
}

More resources

🐛 Testing

vendor/bin/phpunit

🤝 Contributing

Please see CONTRIBUTING for details.

🔒 Security

If you discover any security related issues, please email alex@renoki.org instead of using the issue tracker.

🎉 Credits