Add-On Development and Permissions

Hi there,

I’m just developing an online spreadsheet add-on for analysts to edit results before submission. I’m trying to ensure that only authenticated users can access the page, but I’m struggling to properly inherit permissions from SENAITE but I only seem to have access to standard Zope permissions.

This is my first time developing with Plone, if you have any tips or examples it would help me immensely.

Thanks!

May I ask why you need such a view?. I mean, why SENAITE’s vanilla view for results entry (Worksheet) does not fit well?

The short answer is: the lab staff want it while we transition from their current system to SENAITE.LIMS.

The long answer is:
I’m tasked with replacing their custom made an Analytical Data Management System (ADMS) with SENAITE.LIMS. This ADMS has a standard table format that allows the analysts to copy and paste their results in bulk, and they are very accustomed to this. The add-on I’m developing will provide this functionality to the analysts, without resorting to locally stored spreadsheets. I’ve been given a limited time budget to complete the integration of the instruments, and developing custom export templates and procedures per instrument would take too long.

Here’s what I was proposing:

  1. Analyst logs in, navigates to the view, and pastes the data into the spreadsheet
  2. Analyst selects their worksheet from a dropdown (restricted to those assigned to the user)
  3. Analysts are prompted to pick a LIMS registered Sample ID/Reference Sample to replace each of the Sample IDs from their instrument.
  4. Analysts are then prompted to pick a LIMS analysis service keyword to replace the Analyte headings from their instrument , while ignoring columns that contain superfluous data
  5. Once they confirm the data, the add-on produces a CSV of the results, places it into the configured auto-import folder for that instrument, then calls the auto_import method

A very special scenario imo. I think creating a script to directly import the data from those files might be simpler. If I understand correctly, those files come from instruments, so I guess you can assume that the structure of the file will always be the same. Therefore, you could automatically process the data in accordance on import. If the imported data still needs to be reviewed and modified by analysts later, you could create a specific view for that and submit results after analyst reviewal only.

Back to the question, SENAITE uses the permissions machinery provided by Zope and Plone. Is not easy stuff, cause every single object registered in the system has its own permission mapping, that in turn depends on it’s status based on the workflow(s) the object is bound to.

This is the default roles mapping for SENAITE:

If roles for a given permission are not overriden through workflow definition for a given object or its hierarchy, these are the permissions that apply to a given role.

Regarding workflows and permissions overriding, let me give you an example. setup folder (and therefore, all the contents/objects inside, as long as they are not bound to a specific workflow) is governed by this workflow:

Note that for any given status (in this case, the object can only have the “active” status), an object can have different permissions. And this is precisely governed by its workflow definition.

So, say an AnalysisService type, that belongs to the folder analysis_services, that in turn, belongs to the folder setup. In this case, an object of this type is bound to the workflow https://github.com/senaite/senaite.core/blob/master/bika/lims/profiles/default/workflows/senaite_one_state_workflow/definition.xml , that might override the permissions for any given staus inherited from the previous workflow.

The mappings between object types and workflows is define here: https://github.com/senaite/senaite.core/blob/master/bika/lims/profiles/default/workflows.xml

As you see, permissions machinery in SENAITE is quite powerful, but at the same time complex. And we only grasped the surface here (i.e, we haven’t talked about acquisition).

I think the best approach in your case is to just create a BrowserView and manually check if the user belongs to the role analyst programmatically. Something like:

from bika.lims import api
from bika.lims.api import security

allowed = ["Manager", "LabManager", "Analyst"]
user_id = api.get_current_user().id
user_roles = security.get_roles(user=user_id)
allowed = any(map(lambda role: role in allowed, roles))
if not allowed:
    return "Not valid user"

Further reading:

Thank you very much for the thorough reply, I really appreciate it.

A very special scenario imo. I think creating a script to directly import the data from those files might be simpler. If I understand correctly, those files come from instruments, so I guess you can assume that the structure of the file will always be the same. Therefore, you could automatically process the data in accordance on import.

Yes, an annoyingly special scenario. I’m building something similar to that script you suggest as a part of this add-on, as each time the instrument results are imported I’ll be storing the analyte and reference sample names as a template for the next import. Eventually, when they start using the Sample IDs, the review step could be bypassed and the script could be used to pull from the instrument files directly.

It does seem much simpler to check the roles directly instead of using the permissions, that’s definitely the direction I’ll take on this. Thanks again!

Forgive me for my ignorance, I hate to lean on you guys for stuff I should know as a developer but as a noob, I’m struggling with something:

How do I send my authentication details along with a jsonapi query? Obviously when I send it from a logged in browser session it works fine, but when my script sends the request it’s responding as though I’m not logged in.

Here’s the garbage:

		query = "%s/@@API/senaite/v1/search?portaltype=Client&catalog=bika_setup_catalog" % self.context.absolute_url()
	clients = urllib2.urlopen(query)
	self.role_mess = clients.read()

*EDIT: that code snippet had the wrong query, here’s the right one:

query = "%s/@@API/senaite/v1/search?portal_type=Client" % self.context.absolute_url()

Hi @alexanderstrand, I use requests package instead of urllib2. Note that Client portal type lives in portal_catalog, which is the default catalog the json api uses when searching. Thus, you don’t need to specify a catalog for this query.

Also note you are trying to fetch data from the same instance in which you are running this code. If this is what you really want, then I suggest to not use the json api, rather directly query the catalog and/or use the api functions.

Something like the following (not tested) should work:

import json
import requests

class MySomething(object):

    session = None

    def __init__(self, host):
        self.host = host

    def do_something(self):
        params = {"portal_type": "Client", }
        clients = self.search(params)
        clients = clients.get("items")
        # Do something here with your clients

    def search(params):
        response = self.get("search", params=params)
        if not response:
            return {}
        return json.loads(response.text)

    def get(self, action, params=None):
        url = "{}/@@API/senaite/v1/{}".format(self.host, action)
        response =  self.session.get(url, params=params)
        print "[GET] {}".format(response.url)
        if resp.status_code != 200:
            print "[ERROR] Got {}".format(response.status_code)
            return None
        return response

    def auth(self, user, password):
        self.session = requests.Session()
        self.session.auth = (user, password)    
        response = self.get("auth")
        if not response or response.status_code != 200:
            print "[ERROR] Not authenticated"
            self.session = None


if __name__ == "__main__":
    url = "http://instance_to_get_info_from/senaite"
    user = "user"
    password = "password"

    mysomething = MySomething(url)
    if mysomething.auth(user, password):
        mysomething.do_something()
    else:
        print "[ERROR] Cannot authenticate"

See requests's documentation for more info: https://requests.readthedocs.io/en/master/

Hi @xispa, thanks for the quick response!

Indeed I suspected fetching using the json api within the instance wasn’t correct, but my efforts to decipher the methods from reading the code in senaite.core has been fruitless.
I also struggled to add the requests package when I first tried, which started me down the RestrictedPython rabbit hole.

In your example, it looks like I’d have to store the username and password to authenticate the request. I’m already using api.get_current_user().id to get the current user, could I also use the api to pass the session details directly and avoid storing the username and password? Otherwise it looks like I’d have to create a new user just for my script to upload data.

Starting to have positive results using bika.lims.api, thanks for your help!

I truly believe you should not use json api to accomplish your use case, rather use the functions provided by core’s api directly. For instance, if you want to get all clients, you can just do a simple search like this:

from bika.lims import api
query = {"portal_type": "Client" }
brains = api.search(query, "portal_catalog")
clients = map(api.get_object, brains)

Worth to read (specially to understand how searches, catalogs, brains and obejcts work): https://docs.plone.org/4/en/develop/plone/searching_and_indexing/query.html

I 100% agree, I’m now getting what I need using the core api. I was stuck on JSON as I’m more familiar with it but I’ve abandoned it now.

Thanks again!