I have an admin gate defined in my AuthServiceProvider that is used to add global query scopes to some models. Suppose I had models A, that is observed by an Observer (registered in AppServiceProvider), and B, that makes use of the admin gate to add global query scopes.
// app/Providers/AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
public function boot()
{
Gate::define('admin', [static::class, 'admin']);
}
public static function admin(User $user): bool
{
return $user->group->name === 'Admin';
}
}
// app/B.php
class B extends Eloquent
{
public static function boot()
{
parent::boot();
if (!Gate::allows('admin')) {
static::addGlobalScope('public', function ($query) {
$query->where('public', true);
});
}
}
}
Up to this point everything worked fine. Then I added a model C that has an Observer and uses the admin gate. As C::observe() fires C::boot() and the AppServiceProvider is registered before the AuthServiceProvider the gate was not defined and I extracted the Observer registration to a new ObserverServiceProvider that is registered after AuthServiceProvider.
// app/C.php
class C extends Eloquent
{
public static function boot()
{
parent::boot();
if (!Gate::allows('admin')) {
static::addGlobalScope('public', function ($query) {
$query->where('public', true);
});
}
}
}
// app/Providers/ObserverServiceProvider.php
class ObserverServiceProvider extends ServiceProvider
{
public function boot()
{
A::observe(AObserver::class);
C::observe(CObserver::class);
}
}
// config/app.php
'providers' => [
//...
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
//...
App\Providers\ObserverServiceProvider::class,
]
My problem:
The observers for A and C are still working, as well as the admin gate in B's boot() method, but Gate::allows('admin') in C always returns false without even calling the gate function.
Adding a var_dump(Gate::has('admin')) in C::boot() outputs true and using #can('admin') later in the View during the same request works fine as well, so the gate is definitely defined and working in principle.
The authorization gate cannot be called as the session data (and therefore the authenticated user) is made available by the StartSession middleware, which runs after the service providers.
The problem can be solved by putting the Gate::allows() check inside the anonymous function, so it is only executed when building a query:
// app/C.php
class C extends Eloquent
{
public static function boot()
{
parent::boot();
static::addGlobalScope('public', function ($query) {
if (!Gate::allows('admin')) {
$query->where('public', true);
}
});
}
}
Related
While using Laravel Actions Package the model is not binding and returning an empty array.
What am I missing?
namespace App\Actions\User;
use App\Models\User;
use Illuminate\Routing\Router;
use Lorisleiva\Actions\Concerns\AsAction;
class GetUserAction
{
use AsAction;
public static function routes(Router $router)
{
$router->get('users/{user}', static::class);
}
public function handle(User $user): User
{
return $user;
}
RouteServiceProvider
public function boot()
{
Actions::registerRoutes();
}
If I run
public function handle(ActionRequest $request, User $user): User
{
dd($request->route()->parameters());
}
on the handler it does return the id parameter.
I'd like to keep the declaration of the route inside the Action as suggested in the documentation
If I declare it in the routes file it works
Route::get('/users/{user}', GetUserAction::class);
Trying to implement simple access authorization with Lumen. It works when doing the update (PUT) action.
But I would also like to handle accessing for example all articles.
I also tried the viewAny or view policy method but no success.
Router
$router->group(['prefix' => 'api/v1'], function () use ($router) {
$router->get('articles', ['uses' => 'ArticleController#showAllArticles']);
$router->get('articles/{id}', ['uses' => 'ArticleController#showOneArticle']);
$router->post('articles', ['uses' => 'ArticleController#create']);
$router->delete('articles/{id}', ['uses' => 'ArticleController#delete']);
$router->put('articles/{id}', ['uses' => 'ArticleController#update']);
});
AuthServiceProvider
class AuthServiceProvider extends ServiceProvider
{
public function boot()
{
Gate::policy('App\Article', 'App\Policies\ArticlePolicy');
$this->app['auth']->viaRequest('api', function ($request) {
return app('auth')->setRequest($request)->user();
});
}
}
Policies
namespace App\Policies;
use App\User;
use App\Article;
class ArticlePolicy
{
public function showAllArticles(User $user, Article $post)
{
// not working
return true;
}
public function update(User $user, Article $post)
{
// this works
return true;
}
}
Controller
namespace App\Http\Controllers;
use App\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function __construct()
{
$this->middleware('auth:api');
}
public function showAllArticles()
{
$this->authorize('showAllArticles');
return response()->json(Article::all());
}
public function showOneArticle($id)
{
return response()->json(Article::find($id));
}
public function update($id, Request $request)
{
$article = Article::findOrFail($id);
$this->authorize('update', $article);
$article->update($request->all());
return response()->json($article, 200);
}
}
As per the Laravel documentation on Authorization:
"When defining policy methods that will not receive a model instance, such as a create method, it will not receive a model instance. Instead, you should define the method as only expecting the authenticated user:"
public function create(User $user)
So:
public function showAllArticles(User $user)
"As previously discussed, some actions like create may not require a model instance. In these situations, you should pass a class name to the authorize method. The class name will be used to determine which policy to use when authorizing the action:"
$this->authorize('create', Post::class);
So:
$this->authorize('showAllArticles', Article::class);
Laravel 7.x Docs - Authorization - Writing Policies - Methods without Models
Laravel 7.x Docs - Authorization - Authorizing Actions Using Policies - via Controller Helper authorize
No explanation needed.
I followed the documentation for creating a model observer here https://laravel.com/docs/5.5/eloquent#observers.
But when I try and access the authenticated user I get null.
How can I access the authenticated user in the model observer?
<?php
namespace App\Observers;
use App\Customer;
class CustomerObserver
{
public function created(Customer $customer)
{
dd(auth()->user());
}
public function updated(Customer $customer)
{
dd(auth()->user());
}
}
I've also tried this inside the Customer model and it returns null as well.
public static function boot()
{
parent::boot();
self::updated(function ($model) {
dd(auth()->user());
});
}
Ok, so stupid mistake on my end.
Thanks #fubar for the tip.
I was using a custom authentication provider so I needed to do this:
dd(auth()->guard('admin')->user());
Is there a way to implement this
as documented in this link Event of Models
How can i extend those events to fire broadcast pusher data, rather than making separate events for the model
You could do it a number of ways. If you want to catch ALL model events you can register a wildcard listener. In your App\Providers\EventServiceProvider in the boot method register the wildcard listener:
public function boot()
{
parent::boot();
Event::listen('eloquent.*', function (array $data) {
broadcast(new YourModelEventListener($data));
});
}
Or if you want to keep the logic separate for each model you could create a class observer catch all the events and send to your broadcast handler.
Register the observer
class MyModel extends Model
{
protected static function boot()
{
parent::boot();
static::observe(new MyModelObserver);
}
}
Then in the observer:
class MyModelObserver
{
public function broadcast($method, $model)
{
broadcast(new YourModelEventListener($method, $model));
}
public function creating($model)
{
$this->broadcast('creating', $model);
}
public function updating($model)
{
$this->broadcast('updating', $model);
}
}
public MyBroadCastEvent implements ShouldBroadcast {
public function broadcastOn() {
return ['test'];
}
}
public MyBroadCastEventListener {
public function handle(MyBroadCastEvent $event) {
// should i remove that type hint?
// then do something here
}
}
then call this?
broadcast(new MyBroadCastEventListener($data)); ?
i make a serviceprovider and add provider in app.php but how can i use it ?
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Helpers\api\gg\gg;
class ApiServiceProvider extends ServiceProvider
{
protected $defer = true;
public function boot()
{
}
public function register()
{
$this->app->bind(gg::class, function ()
{
return new gg;
});
}
public function provides()
{
return [gg::class];
}
}
gg class is in App\Helpers\api\gg folder and i want use this class everywhere like that
gg::isReady();
app.php
'providers' => [
...
App\Providers\ApiServiceProvider::class,
...
]
homecontroller#index
public function index()
{
//how can use this provider in there ?
return view('pages.home');
}
When you did $this->app->bind(), you've bound an instance of a class to the IoC. When you bind to the IoC you make that available throughout the entirety of the application. HOWEVER:
Your namespaces break PSR-1 compliance. This is because you are not using StudlyCaps.
BAD: use App\Helpers\api\gg\gg
GOOD: use App\Helpers\Api\GG\GG.
Rename your folders/files accordingly. With that sorted, your bind function should actually change to a singleton. This is because you want a persistent state, not a reusable model.
$this->app->singleton(GG::class, function(){
return new GG;
});
You also should not check ->isReady() in every function, that's an example of an anti-pattern. Instead, this should be in a middleware:
php artisan make:middleware VerifyGGReady
Add this to your Kernel:
protected $routeMiddleware = [
//other definitions
'gg_ready' => App\Http\Middleware\VerifyGGReady::class
];
Update the handle() function in your middleware:
public function handle($request, Closure $next) {
if ($this->app->GG->isReady()) {
return $next($request);
}
return redirect('/'); //gg is not ready
});
And then either initialize it in your route groups:
Route::group(['middleware' => ['gg_ready']], function(){
//requires GG to be ready
});
Or directly on a route:
Route::get('acme', 'AcmeController#factory')->middleware('gg_ready');
Or use it in your controller:
$this->middleware('gg_ready');