laravel / nova-issues

554 stars 34 forks source link

Ability to define Nova-specific authorization policies #131

Closed adamthehutt closed 5 years ago

adamthehutt commented 6 years ago

We're using Nova in a multi-tenant application that has existing access policies defined for nearly all models. The rules that make sense in the application's front-end context don't really make sense in the Nova back-end context. It would be helpful if Nova provided a way to override the default access policies with specific ones to be applied only in Nova.

danrichards commented 6 years ago

Have you tried detecting the context in AuthServiceProvider and mounting your policies accordingly?

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [ ... ];

    protected $nova_policies = [ ... ];

    public function boot()
    {
        if (request()->segment(1) == ltrim(config('nova.path'), '/')) {
            $this->registerNovaPolicies();
        } else {
            $this->registerPolicies();
        }
    }

    public function registerNovaPolicies()
    {
        foreach ($this->nova_policies as $key => $value) {
            Gate::policy($key, $value);
        }
    }
}
adamthehutt commented 6 years ago

Thanks. That would work but it's got some code smell to it, IMHO.

srottem commented 6 years ago

This approach doesn't seem to work - the original policies in the $policies member are still loaded.

danrichards commented 6 years ago

@srottem I would check if you have policies being registered elsewhere.

Service providers for third-party packages sometimes register their own. In the Nova context particularly, gates are often registered in ToolServiceProvider for any third-party tools you may have installed.

Additionally, you can debug with dd(\Gate::policies()) and see exactly what is registered at run-time.

In my case, I only register policies when using Nova, otherwise, I don't register any. Good Luck!

srottem commented 6 years ago

The problem is that config('nova.path') only applies to the UI requests - the API calls also need the correct policies applied but they're at 'nova-api'. Making the if statement apply to both appears to work.

jesseleite commented 5 years ago

@srottem I did this to cover both UI and API requests:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    if ($this->isNovaEndpoint()) {
        $this->registerNovaPolicies();
    } else {
        $this->registerPolicies();
    }
}

/**
 * Check if the current endpoint is a nova related endpoint.
 *
 * @return bool
 */
private function isNovaEndpoint()
{
    return starts_with(request()->segment(1), ltrim(config('nova.path'), '/'));
}

Maybe Nova will provide a slicker solution to app vs. nova policies in the future, but this works for now.

mathieutu commented 5 years ago

Actually it doesn't work if you changed your nova path.

mathieutu commented 5 years ago

I made it working with a better hack. Not a real solution, but it works (based on "nova" middleware) 🤷‍♂️.

https://gist.github.com/mathieutu/d742046ca00fad76c78fc3a6334ddadc

3adeling commented 5 years ago

you should check also for 'nova-api' route segment

if('/' . request()->segment(1) == config('nova.path') || request()->segment(1) == 'nova-api') {
            $this->registerNovaPolicies();
} else {
            $this->registerPolicies();
}
yurii-github commented 5 years ago

thanks guys!

my current helper method

    /**
     * Check if the current endpoint is a nova related endpoint.
     *
     * @return bool TRUE if it is Nova, FALSE otherwise
     */
    public static function isNovaEndpoint()
    {
        static $isNovaEnpoint = null;

        if ($isNovaEnpoint !== null) {
            return $isNovaEnpoint;
        }

        // if we've used admin subdomain
        if (Config::get('nova.domain') && Config::get('nova.domain') === Request::getHttpHost()) {
            return $isNovaEnpoint = true;
        }

        // Nova path
        if (Request::segment(1) === Config::get('nova.path')) {
            return $isNovaEnpoint = true;
        }

        // un-documented Nova API calls path!
        if (Request::segment(1) === 'nova-api') {
            return $isNovaEnpoint = true;
        }

        return $isNovaEnpoint = false;
    }
FrittenKeeZ commented 5 years ago

This can be done quite easily with Laravel 5.8 policy resolver.

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $namespace = 'App\\Policies\\';
    Nova::serving(function () use (&$namespace) {
        $namespace .= 'Nova\\';
    });
    Gate::guessPolicyNamesUsing(function ($class) use (&$namespace) {
        return $namespace . class_basename($class) . 'Policy';
    });
}
davidhemphill commented 5 years ago

Use the Nova::serving event to register your authorization policies.

yurii-github commented 5 years ago

@davidhemphill no man, you didn't get the main issue. The main issue is to register ONLY nova policies while having other policies for general app.

pokono commented 4 years ago

Any definitive answer for this?

bilfeldt commented 3 years ago

Use the Nova::serving event to register your authorization policies.

Dear @davidhemphill I know this is an old issue (and closed) but maybe worth considering in the upcoming Laravel upgrade that the method proposed above (guessing policies in a separate nova namespace only for Nova requests) is not a viable solution for those using Laravel Octane:

Octave Docs: For example, the register and boot methods of your application's service providers will only be executed once when the request worker initially boots. On subsequent requests, the same application instance will be reused.

I have tried discussing why this feature is relevant here but the essence is:

Possible solutions include:

  1. Allow for callbacks after gate check. That way it would be possible to check if for example a post is published right after checking if the user can edit it using PostPolicy@update keeping business logic (only non-published posts can be updated) separate from the authorisation (can user edit posts)
  2. Have separate policies for Nova requests (that could easily extend or fallback to the normal policies)
  3. Place business logic inside Policies but only using it for Nova requests by enclosing it in Nova::whenServing(function (NovaRequest $request) { ... }
bilfeldt commented 2 years ago

@davidhemphill no man, you didn't get the main issue. The main issue is to register ONLY nova policies while having other policies for general app.

Note that this has been made available for Nova specific Observers - see docs:

If you would like to attach an observer whose methods are invoked only during Nova related HTTP requests, you may register observers using the Laravel\Nova\Observable class in your application's NovaServiceProvider:

williamengbjerg commented 2 years ago

@davidhemphill no man, you didn't get the main issue. The main issue is to register ONLY nova policies while having other policies for general app.

Note that this has been made available for Nova specific Observers - see docs:

If you would like to attach an observer whose methods are invoked only during Nova related HTTP requests, you may register observers using the Laravel\Nova\Observable class in your application's NovaServiceProvider:

Been trying this, but doesn't look to exist.

problem9 commented 2 years ago

if someone is stuck on this issue, I think the best approach is to use NovaServiceProvider, when you want to add, or override existing gate policies.

use App\Models;
use App\Nova\Policies;

protected $policies = [
   Models\User::class => Policies\UserPolicy::class,
];

public function boot()
{
    parent::boot();

    Nova::serving(function (ServingNova $event) {
        $this->registerPolicies();
    });
}

 public function registerPolicies()
{
    foreach ($this->policies() as $key => $value) {
        Gate::policy($key, $value);
    }
}

/**
 * Get the policies defined on the provider.
 *
 * @return array
 */
public function policies()
{
    return $this->policies;
}

I don't think there is a way to disable frontend policies without completely disabling authorization in nova resource. right now I am just overriding conflicting frontend policies in administration.

I hope in nova v4 will be better way :)

if you can, you can disable authorization in nova resource

public static function authorizable()
{
    return false;
}
danutavadanei commented 1 year ago

This can be done quite easily with Laravel 5.8 policy resolver.

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $namespace = 'App\\Policies\\';
    Nova::serving(function () use (&$namespace) {
        $namespace .= 'Nova\\';
    });
    Gate::guessPolicyNamesUsing(function ($class) use (&$namespace) {
        return $namespace . class_basename($class) . 'Policy';
    });
}

Following up on @FrittenKeeZ solution I am using something similar as a workaround for when you have the policies in the Nova directory:

class AuthServiceProvider extends ServiceProvider {
    protected string $namespace = 'App\\Policies\\';

    protected string $novaNamespace = 'App\\Nova\\Policies\\';

    public function boot()
    {
        Gate::guessPolicyNamesUsing(
            fn ($class) => $this->namespace() . class_basename($class) . 'Policy'
        );
    }

    protected function namespace(): string
    {
        return Nova::whenServing(
            fn () => $this->novaNamespace,
            fn () => $this->namespace
        );
    }
}
pokono commented 1 year ago

How do you guys handle this in L10? Policies are registered automatically.

HassanElshazlyEida commented 1 year ago

if you are using Multi tenancy and you want to define policies path for tenant you could also use this way

public function boot(): void
    {
        $namespace = 'App\\Policies\\';
        Nova::serving(function (ServingNova $request)use(&$namespace) {
            if (tenancy()->initialized) {
                $namespace .= 'Tenant\\';
            }
        });
        Gate::guessPolicyNamesUsing(function ($class) use (&$namespace) {
            return $namespace . class_basename($class) . 'Policy';
        });
    }
rderks88 commented 4 months ago

This is an old thread, but was still a pressing shortcoming of Nova for us.

Our usecase:

We have websites in our system that people "own". Obviously they can view and edit them. But we have a HelperResource which extends those websites and sort of "showcases" the websites to a larger group. You can argue you should use lenses for that, but still then, we want to separate the policy logic for the "public" variant from the "owner" variant.

Our solution:

We created a trait which allows you to set the policy to be used in any Nova/Resource as follows. Curious what you guys think. So all you need to do is use the trait, and define protected static ?string $policy = WebsitePolicy::class;.

<?php

namespace App\Nova\Contracts;

use Gate;
use Illuminate\Http\Request;
use Laravel\Nova\Actions\Action;
use Laravel\Nova\Actions\DestructiveAction;
use Laravel\Nova\Http\Requests\NovaRequest;

trait CanDefinePolicy
{
    protected static ?string $policy = null;

    /**
     * Here is some f*cking magic.
     * To be able to override policies per resource, we temporarily set the policy for
     * the related model to the policy defined in the static $policy var.
     * We need to make sure to set it back to the default too, otherwise these policies "stick".
     * @return null
     */
    protected static function before()
    {
        if(!static::authorizable()){
            return null;
        }

        /**
         * When policy is null, it just sets the policy to null
         * This will result in it being found later on by the Gate
         *
         * Do take into account, this code interferes with manually assigning
         * policies in the AuthServiceProvider
         */
        Gate::policy(static::$model, static::$policy);
    }

    /**
     * Determine if the resource should be available for the given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function authorizeToViewAny(Request $request)
    {
        static::before();
        parent::authorizeToViewAny($request);
    }

    /**
     * Determine if the resource should be available for the given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    public static function authorizedToViewAny(Request $request)
    {
        static::before();
        return parent::authorizedToViewAny($request);
    }

    /**
     * Determine if the current user can create new resources.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    public static function authorizedToCreate(Request $request)
    {
        static::before();
        return parent::authorizedToCreate($request);
    }

    /**
     * Determine if the current user can replicate the given resource or throw an exception.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function authorizeToReplicate(Request $request)
    {
        static::before();
        parent::authorizeToReplicate($request);
    }

    /**
     * Determine if the current user can replicate the given resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    public function authorizedToReplicate(Request $request)
    {
        static::before();
        return parent::authorizedToReplicate($request);
    }

    /**
     * Determine if the current user can delete the given resource or throw an exception.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function authorizeToDelete(Request $request)
    {
        static::before();
        parent::authorizeToDelete($request);
    }

    /**
     * Determine if the current user can delete the given resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    public function authorizedToDelete(Request $request)
    {
        static::before();
        return parent::authorizedToDelete($request);
    }

    /**
     * Determine if the user can add / associate models of the given type to the resource.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Illuminate\Database\Eloquent\Model|string  $model
     * @return bool
     */
    public function authorizedToAdd(NovaRequest $request, $model)
    {
        static::before();
        return parent::authorizedToAdd($request, $model);
    }

    /**
     * Determine if the user can attach any models of the given type to the resource.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Illuminate\Database\Eloquent\Model|string  $model
     * @return bool
     */
    public function authorizedToAttachAny(NovaRequest $request, $model)
    {
        static::before();
        return parent::authorizedToAttachAny($request, $model);
    }

    /**
     * Determine if the user can attach models of the given type to the resource.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Illuminate\Database\Eloquent\Model|string  $model
     * @return bool
     */
    public function authorizedToAttach(NovaRequest $request, $model)
    {
        static::before();
        return parent::authorizedToAttach($request, $model);
    }

    /**
     * Determine if the user can detach models of the given type to the resource.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Illuminate\Database\Eloquent\Model|string  $model
     * @param  string  $relationship
     * @return bool
     */
    public function authorizedToDetach(NovaRequest $request, $model, $relationship)
    {
        static::before();
        return parent::authorizedToDetach($request, $model, $relationship);
    }

    /**
     * Determine if the user can run the given action.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Laravel\Nova\Actions\Action  $action
     * @return bool
     */
    public function authorizedToRunAction(NovaRequest $request, Action $action)
    {
        static::before();
        return parent::authorizedToRunAction($request, $action);
    }

    /**
     * Determine if the user can run the given action.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Laravel\Nova\Actions\DestructiveAction  $action
     * @return bool
     */
    public function authorizedToRunDestructiveAction(NovaRequest $request, DestructiveAction $action)
    {
        static::before();
        return parent::authorizedToRunDestructiveAction($request, $action);
    }

    /**
     * Determine if the current user has a given ability.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $ability
     * @return void
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function authorizeTo(Request $request, $ability)
    {
        static::before();
        parent::authorizeTo($request, $ability);
    }

    /**
     * Determine if the current user can view the given resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $ability
     * @return bool
     */
    public function authorizedTo(Request $request, $ability)
    {
        static::before();
        return parent::authorizedTo($request, $ability);
    }
}