Closed steven-backerclub closed 4 years ago
Hi. This isn't really an issue for this package... the exact same issue would apply if you were using Blade templates to render pages that listed e.g. products or coupons. The question is effectively how to efficiently query the database based on access rights.
One thing we do is use HTTP middleware to apply global scopes to model classes. I.e. a model global scope does not need only to be applied via a model's boot
method. As you mention, using the boot
method means you have to then worry about it everywhere else in the app. So we've found that model scopes applied via HTTP middleware is an effective way of scoping access to models only for specific HTTP requests. Just use Model::addGlobalScope()
within the middleware.
That middleware would not have to apply any scopes if the user is an admin. (Presuably that's fairly easy to check in your app's logic).
Then if they are not an admin, it'd need to apply 3 scopes - one scope to each model class. Then each of those scopes would have to limit the queries for the respective models, presumably using Eloquent relationship existence methods (e.g. whereHas()
) to limit the queries using relationships. The logic for the query is described in the bullet points for each class that you mention at the start of your issue.
Hopefully that helps - let me know how you get on!
Thank you for the reply, @lindyhopchris. I do recognize this wasn't an issue for the package, but was simply curious for any pointers due to the "flexibility" the package/JSON API can provide the client while still being constrained to a limited set of endpoints.
Glad to hear the model-level Global Scope is a sensible avenue to pursue, with the option of applying them selectively via route middleware. We'll rock and roll with that concept.
Cheers.
First time posting an issue for the package, so I want to start by saying how much I appreciate the work that's gone/going into it. 👍 We're using it for a business application and are grateful to have the head start it provides.
Now onto the question...
In our application, we provide a number of resources that need to have their exposure controlled outside of authorizers/policies. Let's imagine this hypothetical example:
The application is a multi-vendor marketplace. It has
users
, some of which may ownproducts
(aka, be a vendor). Aproduct
hasManyvariants
. And finally, a vendor can choose to createcoupons
that relate to one or morevariants
. In summary, we may have an Eloquent setup that roughly resembles$user->products->variants->coupons
.Here's the exposure control part:
A
product
should only be exposed if:published
attribute istrue
.A
variant
should only be exposed if:published
attribute istrue
.A
coupon
should only be exposed if:active_at
andexpired_at
attributes.Now let's consider a few requests:
GET .../api/products
How do we accomplish this?
It seems that one potential option is to utilize a global scope in the product's Adapter, so we add a scope that resembles:
Wait... that won't work for a vendor trying to expose the products they own that aren't published yet. To know which products a user owns, we would need
GET ...api/users/{record}/products
, but this will still exclude any non-published products from the response due to our scope here.Ok, so we update it to check if the user making the request is the owner of the resource:
Great. Wait.... not so great. An admin of the app still won't be able to see all of the products. Alright, let's check if the user making the request has the ability to 'viewAny' products. If they do, don't modify the $builder instance at all. If they don't, apply the scoping:
A bit of an odd scope, but I suppose it'd work. If the $user is an admin and can 'viewAny', no scope is added to the $builder. Otherwise, we are checking for the published attribute or a users relation matching the authenticated user. It seems weird to be performing a Gate check for a scope, but alas it would get the job done.
GET ...api/products/{id}?include=variants.coupons
orGET ...api/products/{id}
+GET .../api/products/{id}/variants?include=coupons
.coupons
will be eager-loaded on the Variant Models.->orWhereHas('variants.products.users' ...
to allow the vendor to expose their own coupons even if they aren't currently active. This is a multi-nestedSELECT * WHERE (EXISTS...
that seems silly to have in place when <.1% of the requests will actually be from the vendor. It'll be mainly guests/users making such a request, which means we're really only concerned with the date constraint most of the time.booted()
method, which means we have to keep this in mind everywhere in our app. All of those cron jobs that iterate through products to do various things - yup, need to handle the global scopes.GET ...api/variants/{record}/coupons
foreach variant. However, if a product has 25 variants, that's less than ideal.->orWhereHas('owning-user...'
from the scopes, as that will leave us in a position where admins and vendors can't reveal the products that don't fall under the "normal user" exposure requirements.I hope this is a clear enough example to illustrate the situation.
To cover a few other considerations I've had:
GET ...api/users/{record}/products/{product}/....
and permit relation gathering on that route without scope concerns. As long as the user making the request matches the {record} model binding, you can be sure they are only exposing related entities that they own. However, the spec and package don't permit this.api/admin/...
route group with a middleware to check the user's role?GET ...api/coupons?filter[relatedToProduct]={id}&filter[withInactive]=1/0
and use the coupon's Validator/Authorizer to confirm if the user is allowed to have the combination of filter attributes provided. This is similar to how we handled soft-deleted models and determining if a user is allowed to have thefilter[withTrashed]
option. However, our "real life" app has numerous resources with exposure requirements and performing individual requests for each resource seems like a lot of extra load on the servers. If we were to adopt this approach, we'd need to do 6-10 additional requests when loading a typical 'product' page.In conclusion, I'm hoping someone can shed some light on how they solve this dilemma. I feel like this would be a pretty common challenge to face, as lots of apps have entity relationships where anyone is "authorized" to ask for the records, but your response will limit which records are returned based on the user's exposure permissions.