Jump to content

Symfony User Roles


NotionCommotion

Recommended Posts

While I am impressed with much of Symfony, the needing to know everything the "symfony way" is a little annoying.  For an application, I will have several types of users:

  • Admin Users.  They can create a project, add new vendors, assign vendors to a given project, add new normal or admin users, etc.
  • Vendors.  They can view projects which they are assigned to and upload documents for that project.
  • Normal Users.  They will have the ability to view documents related to a given project which are submitted by vendors.
  • Super Users.  There will be multiple Accounts where all of the above user's will be assigned to a single, and they can only interact with others assigned to that same account.  The super user can create an account and add the initial Admin User to that account.

To expose the data, I am using api-platform.  Currently, I am just trying to create endpoints to retrieve a list of vendors, normal users, etc.

One possible solution I know how to accomplish but don't believe is the correct way is to create an AbstractUser and create VendorUser, NormalUser, etc which extend AbstractUser.  Doing so will create routes for each of these user types and meet my immediate needs.

Alternatively, I can create only a single User entity, and then assign roles such as ROLE_ADMIN, ROLE_VENDOR, ROLE_USER, or ROLE_SUPER.  Ideally, I could then add new routes such as /vendors instead of making it /users?type=vendor.

Or maybe I should be doing something totally different.

If I was building this from scratch, I can definitely figure something out, however, would like to take advantage of symfony and do it their way.  But I am a complete newbie when it comes to symfony, and don't even know where to start, and would appreciate a strategy to go which won't lead me down to bad of wrong path.

Link to comment
Share on other sites

Sounds like Roles is what you want.  Define various roles that represent the actions a user can take and check for those in your code.  Then you can create meta roles that are combinations of those actions and assign those to the users.

For example, I have a small application to handle student transcripts.   Any user that can login can lookup/view students and add notes.  Other limited actions include importing/editing student/course data, running official/unofficial transcript reports, and managing users.  For that I have a role setup such as:

  role_hierarchy:
    ROLE_RUN_UNOFFICIAL:
      - 'ROLE_USER'
    ROLE_RUN_OFFICIAL:
      - 'ROLE_RUN_UNOFFICIAL'
    ROLE_MANAGE_USERS:
      - 'ROLE_USER'
    ROLE_MANAGE_STUDENTS:
      - 'ROLE_USER'
    ROLE_DEVELOPER:
      - 'ROLE_USER'
      - 'ROLE_RUN_OFFICIAL'
      - 'ROLE_RUN_UNOFFICIAL'
      - 'ROLE_MANAGE_USERS'
      - 'ROLE_MANAGE_STUDENTS'
      - 'ROLE_MANAGE_COURSES'
    ROLE_MANAGE_COURSES:
      - 'ROLE_USER'

ROLE_USER is applied to every user and is your basic "is a user" role and can search/view student data.

ROLE_RUN_UNOFFICIAL applies to users who can run unofficial transcript reports.   Add ROLE_USER below it means that anyone who has this role also has ROLE_USER

ROLE_RUN_OFFICIAL applies to users can run official reports.  Being able to run official reports can also run unofficial reports so they inherit that role as well (and by extension, ROLE_USER).

ROLE_DEVELOPER is a special role for myself and a few others that basically opens up everything.

Once you setup your role hierarchy as you need it then you can use them either in your security configuration as needed to limit access or check in your code using the isGranted controller method.

if ($officialButton->isClicked()){
    if (!$this->isGranted('ROLE_RUN_OFFICIAL')){
        throw $this->createAccessDeniedException();
    }

    return $this->processTranscriptRequest(Mode::OFFICIAL, $data);
}

In your templates, you can use the is_granted test to conditionally show things:

<td>
   {% if is_granted('ROLE_RUN_OFFICIAL') %}
      {{ form_widget(form.official, {
         'attr': {
            'class': 'button'
         }
      }) }}
   {% endif %}
</td>

 

Link to comment
Share on other sites

Thank you kicken,

How long did it take you to get up to speed with Symfony?  Any particular good resources you used?

I've read about a couple philosophies one could take regarding roles, and was leaning in the direction of what you are doing.  Now no longer leaning!

Any thoughts how you would just return of your users which have been assigned the ROLE_DEVELOPER and similarly all the users who have not been assigned that role?

I take it is_granted() is something one would on a web application and not just an API.  Does it make sense to provide something in the JSON response so that similar functionality can still be performed?

Link to comment
Share on other sites

3 hours ago, NotionCommotion said:

How long did it take you to get up to speed with Symfony?  Any particular good resources you used?

Hard to really say, it's just an ever ongoing process really.  I still wouldn't consider myself any kind of expert.  I'd say I just have an average knowledge of how it works and how to use it.  I also don't use it a lot either.  I have a few smaller sites that use it and work on them occasionally but probably at least 80% of my time is spent maintaining/improving the legacy code in a decade old website.

I'd say it was probably at least six months or so, maybe a year before I'd say I had a decent grasp on how to actually work with Symfony instead of against it.

For example, the forms component was annoying when I first started because I had no idea how to use it and be able to control how the forms were rendered so I frequently just wrote the form HTML myself and processed the data by grabbing it from the Request object just like how I would in in the old site using $_GET/$_POST.  Every time I needed a form though I'd try and do it the Symfony way first and eventually I figured it out and started converting all the old forms.  And only in the last year or so have I gotten to the point where I fell like I'm really taking advantage of using services and dependency injection by defining my controllers as services and moving more of my logic out of the controllers and into other service classes.

As far as resources go there's nothing in particular.  Just google and the source code for the most part.  Whenever I get stuck or want to do something I am unsure of I start dissecting the source code and/or search google for help.  The official documentation is ok and generally worth looking through but I find it too shallow and doesn't go into enough detail for things.  It's hard to navigate also, even if I know something is in the official documentation (ie, form control constraints) it's far easier to find it via google than to go to the Symfony site and try to find it.

Analyzing the source is a great way to get things figured out, even if it's not the fastest.  It has the additional benefit of demonstrating good design solutions as well.  At lot of what I've learned over the years hasn't really been Symfony specific but more general design principals and how to apply them successfully.  I've been applying a lot of this to that decade old app as I revamp old features.  For example, newer features get implemented in a dependency injection friendly way and the logic is more confined to dedicated and re-usable service classes rather than in each individual page file.  My goal is to eventually morph the website into something more Symfony like and maybe eventually migrate it entirely.

 

5 hours ago, NotionCommotion said:

Any thoughts how you would just return of your users which have been assigned the ROLE_DEVELOPER and similarly all the users who have not been assigned that role?

It'll depend mostly on how you store your users and their associated roles.  For example, if you wanted you could actually make separate classes like you suggested and just have each class can override the getRoles() method to return the appropriate roles.

In my case, I have a single UserAccount class with a property called dynamicRoles which is a list of UserRole objects that define which roles a user has.  Then the UserAccount::getRoles() method iterates over that list and gets the actual role names and returns them.

UserAccount.orm.yml

AppBundle\Entity\UserAccount:
  type: entity
  ...
  oneToMany:
    dynamicRoles:
      targetEntity: UserRole
      mappedBy: user

UserAccount::getRoles()

/**
 * @return string[]
 */
public function getRoles(){
    $roleStrings = $this->dynamicRoles->map(function(UserRole $userRole){
        return $userRole->getRole();
    })->toArray();

    return $roleStrings;
}

Finding which users have a given role then is a matter of querying the DB for the UserRole objects that have the given role then getting the associated user:

$userList = $em->getRepository(UserRole::class)->findBy([
    'role' => 'ROLE_RUN_OFFICIAL'
]);
$userList = array_map(function(UserRole $role){
    return $role->getUser();
}, $userList);

I think that the above could be simplified to remove the UserRole object and just store an array of strings directly on the UserAccount object instead, but that kind of echos back to the previous point of things being an ever on-going learning process.  When I originally wrote this I didn't know as much and needed that extra object.

 

5 hours ago, NotionCommotion said:

I take it is_granted() is something one would on a web application and not just an API.  Does it make sense to provide something in the JSON response so that similar functionality can still be performed?

I would guess you probably wouldn't need to be checking your roles much as you'd likely just configure the different routes to only be accessible by specific roles.  I just mentioned the methods so you'd know about them in case you did need them.  I mostly just use them to show/hide various buttons/links based on what roles a user has on a few select pages.  For an JSON API I don't imagine you'd have much use for them as you'd just control access to the endpoints in your security configuration but now you know if you do need them.

** My apps are all Symfony 3/4 based, I've no idea if anything of the above has changed in newer versions.  Eventually I'll get mine updated and learn what's new.

Link to comment
Share on other sites

Yes, everything often seems like a work in progress.  I think I know the "best" way to do something, go down that path for a while, then learn something new which is "way better", and then get myself in a conundrum whether I continue with my previous approach or go forward with the new approach.  And every time I naively believe that making the change will be easy...  Glad I do, but never easy.

Regarding retrieving a list of a given user type, I will use role the "symfony" way for access control and security purposes, but think extending the basic User class is best (whatever best means).  My rational is only based on roles being stored as JSON in the user table.

As always, appreciate your advice!

Link to comment
Share on other sites

I am still on the fence whether I should have a single User class or two classes VenderUser and OwnerUser (really will call just Vendor and User) which each extend AbstractUser.

If I go with just one class, I can add roles as necessary to control action.  Sure, they might have different methods, but this can be controlled by roles.  And each are intended to be joined to different tables (i.e. Project is owned by one OwnerUser and many VendorUsers are assigned to many Projects), but again this can be controlled by the assigned roles.

Or is this "too" different, and I should have two classes?  I believe doing so is more intuitive and less prone to a corrupt database.  It also makes it easier to have one endpoint to retrieve a collection of Users and another to retrieve a collection of Vendors.

But where do I stop?  I expect you have a single User class and not separate StudentUser and ManagerUser classes, right?  And surely I wouldn't want to create a OwnerUser and then an AdminUser because this truly conflicts with the "Symfony" way of doing it.

So, it comes down to whether OwnerUser (which might or might not have some sort of higher admin role assigned) and VendorUser are fundamentally different enough to warrant separate classes.

What do you think?

Link to comment
Share on other sites

On 2/6/2021 at 8:59 AM, NotionCommotion said:

Project is owned by one OwnerUser and many VendorUsers are assigned to many Projects

Can one user be both an owner of one project and a vendor of a different project?  If so, it'd seem like a single user class with a quality access control system would be the best way to handle things.  For example, rather than give ROLE_OWNER directly to the user give it to a ($user,$project) pair your code you can do something like $project->isOwner($user) to check (perhaps with a voter to integrate it into Symfony's default access control system).

On 2/6/2021 at 8:59 AM, NotionCommotion said:

I expect you have a single User class and not separate StudentUser and ManagerUser classes, right?

My Symfony system doesn't allow student's to login so there's only one kind of user.  That decade old application does have separate student/staff users which if converted directly to a Symfony setup would result in two distinct classes.   If I were going to move the system over I'd probably combine them as at the login/user account level there's really not much difference between the two.   The things that are different I'd probably try and factor out into something separate.  I'd have to spend a lot more time thinking about and maybe experiment some on that decision though.

 

Link to comment
Share on other sites

18 minutes ago, kicken said:

Can one user be both an owner of one project and a vendor of a different project?

Currently, no, but like life, that might change.

19 minutes ago, kicken said:

perhaps with a voter to integrate it into Symfony's default access control system)

I've seen mention of this but haven't looked into.  On my list...

20 minutes ago, kicken said:

 I'd have to spend a lot more time thinking about and maybe experiment some on that decision though.

Me too.

Off topic.  Tampa Bay or KC?

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.