User based access control with label based resource access

Hi everybody,
I have the need to implement an authorization web service that allow users to work with a subset of DICOM resources using the “label-based” access control. My architecture is described in the following: I already have a web application in which a user has logged in and that could expose the REST APIs for the authorization. From this web app, I would open OE2 passing a token generated for the specific user. In orthanc configuration I have the following lines:

"AuthenticationEnabled" : false,
 "Authorization" : {
        "WebServiceUserProfileUrl" : " ROOT /user/get-profile",
        "WebServiceRootUrl" : "http://localhost:5000/",
	
	     // The name of the HTTP headers that may contain auth tokens
         "TokenHttpHeaders" : ["auth-token"],
		 
	    // A list of predefined configurations for well-known plugins
         "StandardConfigurations": [               // new in v 0.4.0
             "orthanc-explorer-2",
             "ohif"
         ],

		 "CheckedLevel" : "studies"	
	},

First question: is that possible to open OE2 without passing in the url the relative token (in order to “hide” it? Now i have an url like this: http://localhost:8042/ui/app/?token=1234
Once the user open OE2, through the authorization plugin, I could get the specific user profile using the token passed previously and for this purpose I implemented in my web application the “/users/get-profile” REST API. This is the response produced:

response = {
        "name": "Demo User",   
        "authorized-labels": [              
            label_A
        ],
        "permissions": [    
         		"download",
			"view",
			"settings",
			"edit-labels"
			"upload"
        ],
         "validity": 60
    }

From now on, I want the user can do everything in OE2 (i.e. open OHIF viewer, upload or download data, …). But if I try to download or open OHIF viewer, nothing happens, because the web app receives the call to the /tokens/viewer-instant-link and /tokens/download-instant-link services. I tried to implement the /tokens/{token_type} service, but in the payload I don’t have any information about the user and in the httpHeaders I don’t have any token equal to the one used for accessing to OE2 page. Second question: how can I know that the request for opening OHIF or download a resource comes from the same user, i.e. identified by the first token? Last question: In this context I would like to grant the user to see only the resources tagged with a specific label. Is there a way to add a different label based on different users that upload a DICOM resource automatically (i.e. using a LUA script)? I saw the example code that add label based on the “Institution name” tag, but I want information about the user.
Thank you for your time.
Best regards,
Lorenzo

Hi everybody,
maybe I did too much questions in my previous post. My first (and more important) question is, did I do anything wrong in the configuration for my authorization web service? Is there any way to know which user wants to download or view a DICOM resource? As I described above, If I implement the /tokens/{token_type} service, in the payload I don’t have any information about the user. Did I misunderstood anything? From the Orthanc Book documentation I understood that this service is mandatory for generating tokens that will allow the user to access the specific resouce, but how can I know which is the user that wants to view or download that specific resouce?
Tks a lot,
Lorenzo

Hi,

I have never done it but you might be able to do it in python by overriding the /instances API route, extract the HTTP headers to identify the user, call the native /instances route and then, set the label by calling the Orthanc Rest API ‘without plugin’ to prevent the auth-plugin to block the access to the route.

No, indeed, when someone views or downloads a resource, the access is granted through a resource token that is generated on the fly and which does not include any user information.
Maybe you can also override the /tokens/viewer-instant-link in python too to intercept the HTTP headers identifying the user and call the auth-service directly with a user reference in the payload.

Hope this helps,

Alain

Tks Alain, I was trying to follow another solution using Lua script, but I need your help :slight_smile: . First of all, I could use the basic authentication with users and passwords in orthanc.json, and each user will represent a “group of users” (for example “user1” for users of Institution 1, “user2” for users of Institution 2 and so on). Then, when the “user1” store some data, using the Lua script and the method OnStoredInstance I can identify the username and associate the right label to the resource. Moreover, in this way when I open OE2 in the form ‘http://user1:pwd1@localhost:8042’, the user/password values are authomatically hidden. But how can I “find” only the DICOM resources the user1 has access to? Through the Lua script is that a way to filter the retrieve? I’m trying to implement the IncomingHttpRequestFilter and verify if it is a POST with uri /tools/find, but how can I retrieve and grant access only to specific labeled resources? Maybe is there annother way?
Thanks a lot,
Lorenzo

No, you must override the /tools/find and you can do this only in python or in C++.
You can get some inspiration from the authorization plugin because this is exactly what it is doing: orthanc-authorization: 88ba174ff553 Plugin/Plugin.cpp

It could be usefull, for example, to have a “label field” for queries in OE2, and allow to set its value through a script or some other feature (i.e. using a Lua script) so that when a user performs a query automatically the results are the ones with the related label. What do you think?

We are already doing this in the auth-service where results are filtered against the user’s authorized_labels so we have no plan to implement it another way.

Hi Alain, after doing an evaluation of the possible solutions, I decided to use the advanced authorization plugin. I’ll try to make a list of the steps so you can give me some suggestions, also because there may be something that is not clear to me, keeping in mind that I have my own web application that can both generate tokens based on users, and implement the rest API invoked from the authorization service.

  • When I start Orthanc Explorer, I generate a token for that particular user and open it by passing its value into the url in the format http://localhost:8042/ui/app/?token=1234. My service implements “users/get-profile” REST API and based on the value of the token that arrives, it knows which user it is, which labels it is authorized to see and what permissions it has.
  • If the user wants to download or view a resource, I am forced to implement the /tokens/l<token_type> API, but it does not receive any information about the token in the payload or headers. In order to know that the user is granted, “I should override the /tokens/<token_type> in python to intercept the HTTP headers identifying the user and call the auth-service directly with a user reference in the payload”. Can you give me any tips on how to do that?
  • If I want to set the right label when a user uploads DICOM, “I should override the /instances API route, extract the HTTP headers to identify the user, call the native /instances route and then, set the label by calling the Orthanc Rest API ‘without plugin’ to prevent the auth-plugin to block the access to the route”. Can you give me any tips on how to do that?

Is there anything I misunderstood?
Tks again,
Lorenzo

Hi Lorenzo this sounds what I happen to be working on as well. I had to tweak the config a bit. including customize authorization logic in the external web service.
config here for your reference, but its still a work in progress, with

  1. server-side scripting to label studies based on ref physician tag
  2. custom authorization logic to name against label

Prev discussion here

Hi everybody,
I tried to write a python script that overrides the /instances route and adds a label depending on the token in the request. I would like a suggestion.
My script:

import orthanc
import requests
import json

def OnInstances(output, uri, **request):
    if request['method'] == 'POST':
        headers = request.get('headers', {})
        token_value = headers.get('token')

        response = requests.post('external_web_service_url', json={"token-value": token_value}, headers={"Content-Type":"application/json"})

       if response.status_code != 200:
           orthanc.LogWarning(f"Error in get label: {response.text}")
           orthanc.AnswerBuffer(json.dumps({"Error":"Unable to retrieve label"}), "application/json")
           return

       label = response.json().get("label")
       instance_id = orthanc.RestApiPost('/instances', request['body']

       # Now if I call the following code, it returns Unknown resource, because the instance hasn't been completely stored yet
       # study_info = json.loads(orthanc.RestApiGet(f"/instances/{instance_id}/study"))
       # study_id = study_info.get("ID")
       # existing_labels = study_info.get("Labels",[])

       # if study_id:
           # if existing_labels:
                  # orthanc.LogInfo("Study already has a lable")
          # else:
                  # label_url = f"/studies/{study_id}/{label}"
                  # orthanc.RestApiPut(label_url, '')
               
        orthanc.AnswerBuffer(instance_id, 'application/json')

    else:
         instances = orthanc.RestApiGet(uri)
         output.AnswerBuffer(instances, 'application/json')

Of course, I cannot retrieve the study because the instance wasn’t completely stored. So how can I manage that? Please any suggestion is welcome. I thought to set the label directly on the “ParentStudy” that I have from instance_id. But I don’t think it is correct, because the new instance wasn’t already stored. Am I wrong?
Tks,
Lorenzo

Hi everybody,
I think that following the path of the user based access with the token is not a good idea because I can’t have a reverse proxy and it is too weak in my opinion. For this reason I want to try to follow the basic authentication of Orthanc for different users but I have still the need to label the studies based on the user that loads the data and that want access to them.
I wrote the following lua script for labeling the studies based on the username:

function OnStoredInstance(instanceId, tags, metadata, origin)
    print("OnStoredInstance ...")
	if origin['RequestOrigin'] ~= 'Lua' then
		local username = origin["Username"]
		local label = ""
		if username ~= 'admin' then
			label = username
		end	   
		local parentStudy = ParseJson(RestApiGet("/instances/" .. instanceId .. "/study"))
		if #parentStudy.Labels == 0 and label ~= "" then
			print("OnStoredInstance adding labels to parent study")
			RestApiPut("/studies/" .. parentStudy.ID .. "/labels/" .. label, '')
		end	
	end
    print("OnStoredInstance ... done")
end

and this works fine. Then I overwrite the tools/find with the python plugin.
In the following my code:

import orthanc
import base64
import json

def FindWithLabel(output, uri, **request):
    # The "/tools/find" route expects a POST method
    if request['method'] != 'POST':
        output.SendMethodNotAllowed('POST')
    else:
        # Parse the query provided by the user, and backup the "Expand" field
        query = json.loads(request['body'])
        headers = request.get('headers', {})
        authorization = headers.get("authorization")
        if authorization.startswith("Basic "):
            authorization = authorization.replace("Basic ", "")
        
        #Decode the Base64 string
        decoded_bytes = base64.b64decode(authorization)
        decoded_string = decoded_bytes.decode('utf-8')
        username_password = decoded_string.split(":")
        username = username_password[0]
        password = username_password[1]

        # Call the core "/tools/find" route
        if username != "admin":
            query['Labels'] = [username]
            query['LabelConstraint'] = 'All'
        
        answers = orthanc.RestApiPost('/tools/find', json.dumps(query))       
        output.AnswerBuffer(answers, 'application/json')

orthanc.RegisterRestCallback('/tools/find', FindWithLabel)

Everything works fine. If I want to open a viewer from the Orthanc Explorer 2 and for a user different from the admin, the viewer doesn’t show anything. Is that any other route that I need to implement? Please it is really important.
Tks everybody,
Lorenzo

Hi Lorenzo

I haven’t much to offer, but have you carefully added logs in your endpoints to make sure that they are called as you expect them to be?

Does the query dict look right? Are you positive there’s no exception in there?

What I would certainly do in your case would be to dump the query string and try to execute it with curl. Depending on the outcome, this might give you pointers on the reason there are no results (I would also dump the reply from the standard /tools/find to the console before answering the request)

On an unrelated note, with respect to your previous attempt to override /instances from 4 days ago:

It should definitely work : you may call /instances with RestApiPost from within your plugin. I do not think that there is a risk the instance would not be stored right away : the call is synchronous. I think you probably had another issue.

Also, you should be careful with this:

if request['method'] == 'POST':
    blah()
else
    # assume GET
    getblah()

Do not forget that there are other verbs! Deleting an instance with your plugin installed will not be possible anymore, I am afraid :smile:

–Benjamin

Thank you Benjamin for your answer.
As regards the user access with basic authentication, I tried and as I told you the LUA script for automatic “labeling” of new istances work fine. Regarding the “tools/find” route, I have results when I open OE2, the strange thing is that when I want to view a study, opening it with OHIF Viewer for example, I see the viewer page black and running, but no results. When I use “admin” user, I don’t change the query, so everything works fine. Instead, when I connect with another user credentials, I noticed that the ‘tools/find’ for Series and Instance level are not called, maybe because it isn’t correct to add the Labels in the query when that is performed on levels different from Study? If I insert this change everything works fine. Is that correct my assumption?
Tks,
Lorenzo

The Orthanc book does not make any difference between the four levels with respect to label support. Maybe the /tools/find endpoint does?

https://orthanc.uclouvain.be/book/faq/features.html#id22

I assume you have noticed this disclaimer:

Warning: The database index back-end must implement support for labels. As of writing, only the PostgreSQL plugins in versions above 5.0 and the MySQL plugins in version above 5.0 implement support for labels.

?

Also:

I noticed that the ‘tools/find’ for Series and Instance level are not called, maybe because it isn’t correct to add the Labels in the query when that is performed on levels different from Study?

You are performing the /tools/find request in your wrapper, so I assume you mean that your FindWithLabel function is not called at all.
But then, how could the label addition be the culprit since you are doing it inside your function? Sorry, I don’t get it.

Since you have added labels only at Study level, you must of course only add the labels filter when performing tools/find at Study level.

I won’t say everything will work fine. As stated before, the whole authorization plugin and its integration with OE2 have been designed for user management in keycloak and for opening single resources in viewers based on resource-tokens. You are exploring new usages that we don’t even understand.

BTW, I have still not understood why you seem to re-implement something that has already been done in the auth-service and auth-plugin.

Hi Alain,
sorry maybe I wasn’t clear in my last post. My last choice is to use the basic authentication, not the advanced authorization plugin anymore, allowing access to OE2 and its resources through username and password registered inside the configuration file of Orthanc.

  • As a requirement, I need to identify studies coming from specific “research centers”. For this reason I want to use the “label” info related to each username that uploads the new DICOM resource. So I implemented a LUA Script that attach new label to studies depending on the user logged into OE2.

  • At the same time, I need to filter data when a user wants to query the list of DICOM resources. But in this case, OE2 doesn’t work because this is performed by the authorization plugin that I am not using anymore. So I had to override the route ‘tools/find’ as you suggested, in order to query studies based on the label related to the username.

Do you think that something is not correct? Thank you very much,
Lorenzo

Hi Lorenzo,

Unfortunately, the authorization plugin not only overrides tools/find but also make sure all other accesses are checked as well. e.g, in your system, if a user calls /studies/{id}, he will get access to studies he should not have access to.

So, if you need a label based access control, I would start from the auth-plugin + auth-service with Keycloack that has been developed exactly for that purpose (sample).

I understand that you need to label studies as they are uploaded. If you override the POST /instances route in python, you should get access to the user-token in the HTTP headers. From that token, you should be able to call the auth-service to get the user-profile and then, once you know the user, you should be able to apply the right label.

Hope this helps,

Alain.

Hi Alain, I ame so sorry to bother you :smiley:
Unfortunately, in my scenario I cannot use a reverse proxy (i.e. nginx) and I cannot use Keycloack. The only think I can do is implement an auth-service. So if I use the auth plugin, I cannot use anymore username\password basic authentication and in order to allow a user to open OE2, I am forced to set a token inside the url as a get parameter (isn’t that correct?) and I would like to avoid this solution.
What would you do in my context?
Many many tks,
Lorenzo

Hi Lorenzo

I am trying to understand what you want : you don’t want to use the advanced auth because you cannot use a reverse proxy, is that right?

In that case, you want your users to directly log to Orthanc, with basic auth (if you don’t want to use tokens), and you would like to override all routes so that you only give access to resources that bear a label that “belongs” to the user, is that right? (Where are you going to store the labels that each user is able to use?)

In that case, could you override all the resource routes and perform the same kind of override that you did with /tools/find? That is, extracting the user and filter/block the low-level request based on the user?

?

Hi Benjamin,
yes that’s my scenario. Regarding the labels I want to use a “research center” user, so for example if I am a doctor of the research center 1, my username will be something like “RCenter1”, and it can be used for labeling the resourse that I am going to upload. So I don’t have the need to store in a separate db the association with username and label because they are the same.
What do you mean when you say “could you override all the resource routes and perform the same kind of override that you did with /tools/find” you mean every call to every routs to DICOM resources? Is there a list of those route that I can follow? In your opinion this solution is too weak? Any suggestion?
Tks,
Lorenzo