Accessing user roles in progressively decoupled blocks

I am building a module for farmOS that uses Vue.js to render pages that interact with farmOS via the API using the farmOS.js library. Overall this is working like a charm so far :slight_smile:

One thing I would like to be able to do is to change some the available UI elements based on the role (or permissions) of the currently logged in user (e.g. a button to create a new plant type, or a new location, etc).

The challenge is that there does not seem to be a standard way to obtain role/permission information about the currently logged user via API. The admin user can get role/permission information about their self (and others). However any other user cannot. Note: I only want the currently logged in user to get information about themselves.

I spent some time experimenting with trying to create a view that exposed an API endpoint showing just information about the currently logged user (including their roles and permissions). The effect is the same, the admin user receives a populated roles array in the response, while any other user does not. Apart from granting “User Administer…” permissions to other roles (clearly not a good idea), I could not see any standard way to allow a non-admin user to access their own role or permission information via the API.

Some of my thoughts at this point are:

  • Inject the role/permission information for the currently logged in user into the html page as global variables in the Drupal controller (though I’ve not tried it so not sure if it would work either).
  • Have the Vue app probe farmOS paths and construct its own permission based on which paths are available or not available. (E.g. try to access the http://farmos/admin/structure/taxonomy/manage/crop_family/add page or try to actually create a crop_family via API and see if it is allowed or not.)

I’m hoping others may be able to point out something I am missing or have other thoughts on how this type of functionality could be implemented. Thanks!

2 Likes

Any chance that there could be a permission that would allow a non-admin user to retrieve just their own list of roles/permissions via API?

Would this be a farmOS thing or a JSON:API module thing?

Great question @braught - and not something I’ve dealt with myself, but I might be able to make some educated guesses based on my understanding…

Yea I don’t think there is any built-in way to do this in Drupal. Just like there is no way for a Drupal user to see what roles/permissions they have via the UI (unless they have admin level access to configure those things themselves). These are simply not exposed to users.

Instead, the expectation is that the pages built by Drupal will perform the necessary permission checking and build themselves accordingly.

JSON:API does filter out resources that the user does not have access to. And it will prevent the user from creating resources if they don’t have permission.

That doesn’t help if you are trying to build a UI that shows/hides things based on permission though…

(Also just an aside: best practice is to only build your modules around permissions - not roles. Because roles may differ from one instance to another, and unless your module is the one providing the roles, and is designed to ONLY work with those roles, then there’s no guarantee they will exist. So it’s best to only check if a user has a permission - not a role.)

I think the best solution might be to do something along the lines of what you suggested:

It’s possible to add variables to the global Javascript Drupal.settings variable on the page, which you can then access from your Vue app.

This gives an example using hook_preprocess_page() but you can do something similar within your page controller too:

This is a simple example that you can use in your controller PHP (assuming the build array is $build):

$build['#attached']['drupalSettings']['foo'] = 'bar';

With that you can then run the following in the browser console:

console.log(Drupal.settings.foo)

I probably wouldn’t recommend sticking the full list of user permissions into the page’s JavaScript, though. Something about that feels wrong (and maybe risky from a security perspective). Instead, the best approach might be to set flags for your own app specifically based on permissions. So for example… based on what you described…

Maybe in your controller you could check if they have the necessary permission, and set something like this:

$build['#attached']['drupalSettings']['farmdata2']['create_plant_type'] = $user->hasPermission('create terms in plant_type'); (I didn’t test this so I might have the permission wrong…)

Then you can check for that boolean in your Vue code.

That’s probably how I would go about this! Of course, it’s easier when you are also building the controller, which I think you are in FarmData2.

In completely decoupled apps, where you are not building the page in Drupal, but rendering it on an external URL, it’s a little trickier (eg: Field Kit) because you can’t know the permissions ahead of time.

1 Like

Thanks for this detailed reply! I will give this a go and report back.

1 Like

In the end I decided to have the front end probe for permissions instead of having Drupal embed them into the pages. The main reason for this was to support Vue component testing for elements of the FarmData2 application. Some of the components contain elements that need to be shown/hidden based on the user’s permissions. However, the component tests run outside of farmOS and thus would not have access to the permissions embedded by in the page by Drupal. This is a little less efficient because it requires some additional API calls on page load to check permissions, but doesn’t seem too bad. If necessary, performance can be improved in the future by caching the permissions the first time they are fetched.

So basically, I added a function that probes for each required permission by trying to create an asset/log/quantity/etc… and then checking if it was successful and cleaning up if necessary. The code for one such function is here for reference:

/**
 * Check if the current user has permission to create a new crop (i.e. `taxonomy_term--plant_type`).
 *
 * @return {boolean} true if the current user has permission to create a new crop.
 *
 * @category Permissions
 */
export async function canCreatePlantType() {
  const farm = await getFarmOSInstance();
  const testCrop = farm.term.create({
    type: 'taxonomy_term--plant_type',
    attributes: {
      name: 'Permission Check',
    },
  });

  try {
    await farm.term.send(testCrop);
    await farm.term.delete('plant_type', testCrop.id);
    return true;
  } catch (err) {
    return false;
  }
}

Following conversations during the last dev call (thanks @mstenta, @paul121) a better solution was to create a API endpiont in the farmdata2 module. This turned out to be relatively easy (despite my limited PHP/Drupal experience).

In the module’s routing.yml file:

farm.fd2_permissions_api:
  path: '/api/permissions'
  defaults:
    _controller: '\Drupal\farm_fd2\Controller\FD2_Controller::permissions'
    _title: 'permissions'
  methods:  [GET]
  requirements:
    _user_is_logged_in: 'TRUE'

In the Controller:

...
use Symfony\Component\HttpFoundation\JsonResponse;

...

public function permissions() {
    // List each permissions to be checked here.
    $perms = [
      'create land asset',
      'create plant asset',
      'create structure asset',
      'create terms in tray_size',
    ];

    foreach($perms as $perm) {
      $perm_name = str_replace(' ', '-', $perm);
      $result[$perm_name] = \Drupal::currentUser()->hasPermission($perm);
    }

    return new JsonResponse([ 'permissions' => $result, 'method' => 'GET', 'status'=> 200]);
  }

Can then use a farmOS.js instance to request the permissions:

const resp = await farm.remote.request.get('http://farmos/api/permissions');
console.dir(resp.data.permissions);

Returns a nice JSON structure:

{
      "permissions": {
            "create-land-asset": true,
            "create-plant-asset": true,
            "create-structure-asset": true,
            "create-terms-in-tray_size": true
      },
      "method": "GET",
      "status": 200
}
2 Likes

@braught Nice!

This definitely seems like it’s going to be a common need, if we expect more decouple JS app development to happen.

For apps that connect via OAuth, scopes may be able to provide similar possibilities, but for cases like yours (where it is using the existing cookie authentication) it makes sense that apps will need to be able to ask what permissions are available in order to render UIs accordingly. This might be worth a dedicated forum topic to discuss the possibilities, as well as implications (security and other).

3 Likes