Best way in API-Platform for role based results - php

I am using Symfony 5.4 and API-Platform 2.6 and would like to filter the returns based on user roles.
It is about data created by different institutions. Each of these institutions is allowed to see all their own data, but not the data of the other institutions.
But there is also a role (I call it administrator in the following) that is allowed to see all data, but in anonymized form. Here some fields are missing like for example the name. For data protection reasons it is necessary that the data is already filtered.
Now I am looking for the best way to implement this.
It would be nice if the routes do not have to provide the institution ID, but they are automatically added internally and respected on the server side.
For the administrator role I don't see a really good solution yet.
I am open for solutions, as well as alternatives.
Also please excuse my bad English.

I see many questions in one question here ^^
Identify the institution of the connected user
You could add a relation user->institution, that's a simple solution and you'll be able to retrieve the user's institution from the connected user.
From now how do you know if a user is part of an institution?
Filter the data per institution
To illustrate let's imagine you have a Product with a getter for each property:
id
name
user
institution
You could create a ApiPlatform extension, there is a good example that is similar to your usecase.
Example
class FilterByInstitutionExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(private Security $security)
{
}
/**
* {#inheritdoc}
*/
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
$this->addWhere($queryBuilder, $resourceClass, $queryNameGenerator);
}
/**
* {#inheritdoc}
*/
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
{
$this->addWhere($queryBuilder, $resourceClass, $queryNameGenerator);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, QueryNameGeneratorInterface $queryNameGenerator)
{
$toFilterClasses = [Product::class];
if (!in_array($resourceClass, $toFilterClasses)) {
return;
}
// user isn't connected or anonymous
// UserRoles::ADMIN is pseudo code, replace by whatever your admin role is
if (null === $user = $this->security->getUser() || $this->security->isGranted(UserRoles::ADMIN)) {
return;
}
$institution = $user->getInstitution();
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.institution = :institution', $rootAlias));
$queryBuilder->setParameter('institution', $institution);
}
}
Remove user-sensitive data for admin
In Symfony when you want to modify returned data from the server you generally use denormalizer, you should really check the documentation is pretty well explained. Basically, you want to create a denormalizer for each of your apiplatform resources that contains sensitive user data.
Your denormalizer could look like this. Of course, you'll need to tweak it, it's pseudo code right now :)
class AnonymizedUserDataNormalizer implements NormalizerInterface
{
public function __construct(private NormalizerInterface $normalizer, private Security $security)
{
}
public function normalize($entity, string $format = null, array $context = [])
{
$data = $this->normalizer->normalize($entity, $format, $context);
if (!$this->security->isGranted(UserRoles::ADMIN)) {
return $data;
}
if (isset($data['user'])) {
unset($data['user']['firstName'], $data['user']['lastName']);
}
return $data;
}
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof Product;
}
}
Then you need to register your service because you're using an ApiPlatform service.
App\Serializer\Normalizer\AnonymizedUserDataNormalizer:
arguments:
$normalizer: '#api_platform.serializer.normalizer.item'
All of this is not the only way to go, it's just a proposal to put you on the road. Feel free to ask if something is not clear :)

Related

How can I create a custom validation rule and define it in Service Provider a form?

I'm making a form generator on a project, and we've decided to automatically upload files before the user submit their answers. The problem is then that we can't use a premade rule for the validation to check if the media as been uploaded if it's required, because the field will be empty at the submit. Plus, we also require a media if a radio button as a defined value (for example : do you need to rent a car? Yes/No; if yes, please upload a scan of your driving license).
I've read the documentation about custom validation rules, but the forms and ruleset are defined in a json stored in the db, so I can't call the rule like new CustomValidationRule. I wanted to create a shortcut to make it look like a "real" rule and that could be stored in my json with no problem.
Here is a code that is working to make my custom rule to do what I want.
AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
/**
* Boot the application events.
*
* #return void
*/
public function boot()
{
Validator::extend('media_required_if', function ($attribute, $value, $parameters, $validator) {
if ($validator->getData()[$parameters[0]] == $parameters[1]) {
$model = request()->route($parameters[2]);
$media = $model->getMedia('attachments', ['input_name' => $attribute]);
if (count($media) == 0) {
return false;
}
return true;
}
return true;
}, "The dropzone :attribute must contain at least a file.");
It is invoked like this in the ruleset media_required_if:rent_car,1,model. It's a rule can exclusively for an update, so I can have the model, check if media has been uploaded yet for this attribute (there can be more dropzones, all depending of other rules), and check if there is at least one file store. I need these 3 parameters.
But I don't think AppServiceProvider is the best place to store this rule, and I'd like to only keep the invokation here and the declaration with its snake case name.
Here's what I tried and is not working:
Validator::extend('media_required_if', new MediaRequiredIf, "The dropzone :attribute must contain at least a file.");
namespace App\Rules;
/// uses
class MediaRequiredIf implements DataAwareRule, InvokableRule, ValidatorAwareRule
{
protected $data = [];
protected $validator = [];
/**
* Run the validation rule.
*
* #param string $attribute
* #param mixed $value
* #param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
* #return void
*/
public function __invoke($attribute, $value, $fail)
{
dd($this->data, $this->validator);
// Here I want to put the code that works, but $this->data and $this->value aren't filled.
}
public function setData($data)
{
$this->data = $data;
return $this;
}
public function setValidator($validator)
{
$this->validator = $validator;
return $this;
}
}
I don't understand but $this->data and $this->validator always return [], whatever I do.
I tried to follow the documentation but it's not really clear, and I also tried to follow older documentations (from 8.* to 5.2), but the code is really different and I don't want to make code that will be obsolete in a week. Is it even possible to do that?
Thanks a lot for your answers.
You can use required_if to check the driving licence.
Add a hidden input and check it after the file is uploaded. Then use exclude_if, so it will be excluded from validation.
https://laravel.com/docs/9.x/validation#conditionally-adding-rules

Symfony RBAC - How to add into it programmatically in DDD?

I have written an RBAC (Role-based access control) implementation within my Symfony project a few days ago using Symfony Voters.
This is my universal voter for checking if the member has the ability to, for example, create on a specific endpoint:
class AbilityVoter extends Voter
{
public function __construct(MemberRepositoryInterface $memberRepository)
{
$this-memberRepository = $memberRepository;
}
protected function supports(
string $attribute,
$subject
): bool {
if (!in_array($attribute, ['create'])) {
return false;
}
return true;
}
protected function voteOnAttribute(
string $attribute,
$subject,
TokenInterface $token
): bool {
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
/** #var Member $member */
$member = $user;
return $this->memberRepository->hasAbility(
$member->getId(),
$attribute,
$subject
);
}
}
There is a part I don’t think I've addressed though: how do I hook into that within command handlers or other such places in my code?
On some example endpoints, that means that restricting access to certain endpoints is not enough and there will need to be RBAC-aware logic somewhere in the endpoint that determines whether the request is allowable.
For example, if the member is not allowed to post content for others but the request contains someone else’s member ID, it should be denied, otherwise, it should be allowed.
Can someone suggest the best way to do this and implement proof-of-concept code for the example above? Should I create some custom voters (that extend my base class) and use that? Or do we need to inject a service into the command handler so that it makes that decision?
Can someone help with that? What would be the beast approach in this case? Thanks

Doctrine Querybuilder doesnt return exact matches

I have and api endpoint which should return a user based on his id : user/{id}.
In my controller i have the following code:
/**
* #Api\Get("user/{id}")
* #SWG\Parameter(name="id", type="string", in="path", required=true)
*/
public function getUser(Request $request, string $id) {
$user = $this->getDoctrine()->getManager()->getRepository('App\Entity\User')->findUser($id);
return $this->view($user);
}
In my repository i have the following code:
public function findUser(string $id) {
$qb = $this->createQueryBuilder('a');
$qb->andWhere($qb->expr()->eq('a.id',':id'))->setParameter('id',$id);
return $qb->getQuery()->getOneOrNullResult();
}
it is working fine when getting user/4, but it is also working when getting user/4ruqbu.
I just want to get the user on user/4 and no result on user/4ruqbu. It should return the exact match. I think the problem is that the id parameter is a string. if i change it to int it is working, but i get an error instead of no result if i try user/4eiugq.
Is there a way to handle that?
Your route accepts any string for your id parameter which means that any value will make its way to your repository method. Then if you try to use a string which starts with a numeric value as an integer, you will get that integer.
Try for yourself on 3v4l.org:
var_dump((int)'4ruqbu'); // output: int(4)
You could make use of parameters validation using a Regex:
/**
* #Api\Get("user/{id<\d+>}")
*/
public function getUser(Request $request, int $id) {
// ...
}
This should now fail for URIs such as /user/4ruqbu as it does not meet the route requirements.
Some improvements:
If you installed sensio/framework-extra-bundle you can benefit of the ParamConverter and replace string $id with a user:
public function getUser(User $user, Request $request)
{
// ...
}
Avoid fetching services related to Doctrine with $this->getDoctrine() etc. Use autowiring instead.
If you need the Entity Manager to save/update/remove entities, add a Doctrine\ORM\EntityManagerInterface argument to your controller method. If you need to query for entities using a repository, add an argument typed with its class like App\Repository\UserRepository in your case:
// Example of controller method fetching services by autowiring:
public function myAction(EntityManagerInterface $em, UserRepository $userRepo)
{
// ...
}

doctrine is fetching bigints as string - how to prevent this

I have the following issue:
Doctrine is suppose to update entities that were changed. The problem is that for some reason (maybe legacy 32 bit systems?) the bigint data type is treated as string (as you can see below - this is the bigint type class in doctrine, there are also multiple other conversions to string in doctrine code).
<?php
namespace Doctrine\DBAL\Types;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
/**
* Type that maps a database BIGINT to a PHP string.
*/
class BigIntType extends Type implements PhpIntegerMappingType
{
/**
* {#inheritdoc}
*/
public function getName()
{
return Type::BIGINT;
}
/**
* {#inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getBigIntTypeDeclarationSQL($fieldDeclaration);
}
/**
* {#inheritdoc}
*/
public function getBindingType()
{
return ParameterType::STRING;
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $value === null ? null : (string) $value;
}
}
This results in updating data that should not be updated because the unit of work checker compares data as strict (as it should) resulting in differences (code below).
// skip if value haven't changed
if ($orgValue === $actualValue) { // $orgValue = "3829784117300", $actualValue = 3829784117300
continue;
}
The end result for this is that the following code:
$product = $productRepo->find($id);
$product->setOwnerId($ownerId); // $ownerId = 3829784117300
$this->em->flush();
Is generating a query that does... nothing basically except stressing db (in my case I have a few tens of millions of those per day). The solution for the particular case above...
$product = $productRepo->find($id);
if ((int)$product->getOwnerId() !== $ownerId) {
$product->setOwnerId($ownerId); // $ownerId = 3829784117300
}
$this->em->flush();
Simple right? But... what do you do when you have 2 bigints? Ok... 2 conditions... not a big deal. But what if they are... 90? Ok... we can use reflection go through entity properties and check all.
But... what if somewhere in the relation chain there is another entity that needs to be checked? And the complete solution is that we have to recursively check every attribute of the entity and its children and check for bigints.
But... isn't that what the doctrine unit of work is for? Why do I need to reparse the whole entity and check for something that is already checked for just because bigint is treated as string (resulting in basically duplicating a huge chunk of doctrine code)?
And now the question... how to go around this (bare in mind that I'm not asking for a particular solution for a simple case, I'm asking for a general solution that can be applied to a big code base that is supposed to be mentained for years to come - as you can see above I have solutions but I'm not ok with half jobs and fragile code unless there is really no other way)? I'm looking for maybe a setting that I missed that makes doctrine treat integers like integers and not as strings... stuff like that.
One solution would be to override Doctrine's implementation of bigint type with your own. First, create a class identical to Doctrine's BigIntType, except replacing the cast to string with a cast to int:
class MyBigIntType extends Type
{
/**
* {#inheritdoc}
*/
public function getName()
{
return Type::BIGINT;
}
/**
* {#inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getBigIntTypeDeclarationSQL($fieldDeclaration);
}
/**
* {#inheritdoc}
*/
public function getBindingType()
{
return \PDO::PARAM_STR;
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return (null === $value) ? null : (int)$value;
}
}
Then, register the type in config.yml or doctrine.yaml:
doctrine:
dbal:
types:
bigint: MyBigIntType
I tested this with Symfony 2.8, and this caused to use MyBigIntType for all fields that were of type bigint. Should work with later Symfony versions as well.

Laravel caching authenticated user's relationships

In my application I use Laravel's authentication system and I use dependency injection (or the Facade) to access the logged in user. I tend to make the logged in user accessible through my base controller so I can access it easily in my child classes:
class Controller extends BaseController
{
protected $user;
public function __construct()
{
$this->user = \Auth::user();
}
}
My user has a number of different relationships, that I tend to eager load like this:
$this->user->load(['relationshipOne', 'relationshipTwo']);
As in this project I'm expecting to receive consistently high volumes of traffic, I want to make the application run as smoothly and efficiently as possible so I am looking to implement some caching.
I ideally, need to be able to avoid repeatedly querying the database, particularly for the user's related records. As such I need to look into caching the user object, after loading relationships.
I had the idea to do something like this:
public function __construct()
{
$userId = \Auth::id();
if (!is_null($userId)) {
$this->user = \Cache::remember("user-{$userId}", 60, function() use($userId) {
return User::with(['relationshipOne', 'relationshipTwo'])->find($userId);
});
}
}
However, I'm unsure whether or not it's safe to rely on whether or not \Auth::id() returning a non-null value to pass authentication. Has anyone faced any similar issues?
I would suggest you used a package like the following one. https://github.com/spatie/laravel-responsecache
It caches the response and you can use it for more than just the user object.
Well, after some messing about I've come up with kind of a solution for myself which I thought I would share.
I thought I would give up on caching the actual User object, and just let the authentication happen as normal and just focus on trying to cache the user's relations. This feels like quite a dirty way to do it, since my logic is in the model:
class User extends Model
{
// ..
/**
* This is the relationship I want to cache
*/
public function related()
{
return $this->hasMany(Related::class);
}
/**
* This method can be used when we want to utilise a cache
*/
public function getRelated()
{
return \Cache::remember("relatedByUser({$this->id})", 60, function() {
return $this->related;
});
}
/**
* Do something with the cached relationship
*/
public function totalRelated()
{
return $this->getRelated()->count();
}
}
In my case, I needed to be able to cache the related items inside the User model because I had some methods inside the user that would use that relationship. Like in the pretty trivial example of the totalRelated method above (My project is a bit more complex).
Of course, if I didn't have internal methods like that on my User model it would have been just as easy to call the relationship from outside my model and cache that (In a controller for example)
class MyController extends Controller
{
public function index()
{
$related = \Cache::remember("relatedByUser({$this->user->id})", 60, function() {
return $this->user->related;
});
// Do something with the $related items...
}
}
Again, this doesn't feel like the best solution to me and I am open to try other suggestions.
Cheers
Edit: I've went a step further and implemented a couple of methods on my parent Model class to help with caching relationships and implemented getter methods for all my relatonships that accept a $useCache parameter, to make things a bit more flexible:
Parent Model class:
class Model extends BaseModel
{
/**
* Helper method to get a value from the cache if it exists, or using the provided closure, caching the result for
* the default cache time.
*
* #param $key
* #param Closure|null $callback
* #return mixed
*/
protected function cacheRemember($key, Closure $callback = null)
{
return Cache::remember($key, Cache::getDefaultCacheTime(), $callback);
}
/**
* Another helper method to either run a closure to get a value, or if useCache is true, attempt to get the value
* from the cache, using the provided key and the closure as a means of getting the value if it doesn't exist.
*
* #param $useCache
* #param $key
* #param Closure $callback
* #return mixed
*/
protected function getOrCacheRemember($useCache, $key, Closure $callback)
{
return !$useCache ? $callback() : $this->cacheRemember($key, $callback);
}
}
My User class:
class User extends Model
{
public function related()
{
return $this->hasMany(Related::class);
}
public function getRelated($useCache = false)
{
return $this->getOrCacheRemember($useCache, "relatedByUser({$this->id})", function() {
return $this->related;
});
}
}
Usage:
$related = $user->getRelated(); // Gets related from the database
$relatedTwo = $user->getRelated(true); // Gets related from the cache if present (Or from database and caches result)

Categories