Skip to content

Commit

Permalink
Merge pull request #1349 from psavery/token-authentication
Browse files Browse the repository at this point in the history
Add token authentication option for DICOMweb
  • Loading branch information
manthey authored Nov 21, 2023
2 parents 01b17a3 + 6180029 commit 2712dcf
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 46 deletions.
4 changes: 2 additions & 2 deletions sources/dicom/large_image_source_dicom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def _open_wsi_dicomweb(self, info):
# The following are optional keys
qido_url_prefix=info.get('qido_prefix'),
wado_url_prefix=info.get('wado_prefix'),
session=info.get('auth'),
session=info.get('session'),
)

wsidicom_client = wsidicom.WsiDicomWebClient(client)
Expand All @@ -195,7 +195,7 @@ def _open_wsi_dicomweb(self, info):
requested_transfer_syntax=transfer_syntax)

def _identify_dicomweb_transfer_syntax(self, client, study_uid, series_uid):
# "client" is a DICOMwebClient
# "client" is a dicomweb_client.DICOMwebClient

# This is how we select the JPEG type to return
# The available transfer syntaxes used by wsidicom may be found here:
Expand Down
5 changes: 5 additions & 0 deletions sources/dicom/large_image_source_dicom/assetstore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def createAssetstore(event):
'qido_prefix': params.get('qido_prefix'),
'wado_prefix': params.get('wado_prefix'),
'auth_type': params.get('auth_type'),
'auth_token': params.get('auth_token'),
},
}))
event.preventDefault()
Expand All @@ -53,6 +54,7 @@ def updateAssetstore(event):
'qido_prefix': params.get('qido_prefix'),
'wado_prefix': params.get('wado_prefix'),
'auth_type': params.get('auth_type'),
'auth_token': params.get('auth_token'),
}


Expand All @@ -78,6 +80,9 @@ def load(info):
required=False)
.param('auth_type',
'The authentication type required for the server, if needed (for DICOMweb)',
required=False)
.param('auth_token',
'Token for authentication if needed (for DICOMweb)',
required=False))

info['apiRoot'].dicomweb_assetstore = DICOMwebAssetstoreResource()
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import requests
from requests.exceptions import HTTPError

from girder.exceptions import ValidationException
Expand Down Expand Up @@ -45,10 +46,13 @@ def validateInfo(doc):
if isinstance(info.get(field), str) and not info[field].strip():
info[field] = None

# Now, if there is no authentication, verify that we can connect to the server.
# If there is authentication, we may need to prompt the user for their
# username and password sometime before here.
if info['auth_type'] is None:
if info['auth_type'] == 'token' and not info.get('auth_token'):
msg = 'A token must be provided if the auth type is "token"'
raise ValidationException(msg)

# Verify that we can connect to the server, if the authentication type
# allows it.
if info['auth_type'] in (None, 'token'):
study_instance_uid_tag = dicom_key_to_tag('StudyInstanceUID')
series_instance_uid_tag = dicom_key_to_tag('SeriesInstanceUID')

Expand All @@ -61,7 +65,8 @@ def validateInfo(doc):
fields=(study_instance_uid_tag, series_instance_uid_tag),
)
except HTTPError as e:
raise ValidationException('Failed to validate DICOMweb server settings: ' + str(e))
msg = f'Failed to validate DICOMweb server settings: {e}'
raise ValidationException(msg)

# If we found a series, then test the wado prefix as well
if series:
Expand All @@ -76,10 +81,15 @@ def validateInfo(doc):
series_instance_uid=series_uid,
)
except HTTPError as e:
raise ValidationException('Failed to validate DICOMweb WADO prefix: ' + str(e))
msg = f'Failed to validate DICOMweb WADO prefix: {e}'
raise ValidationException(msg)

return doc

@property
def assetstore_meta(self):
return self.assetstore[DICOMWEB_META_KEY]

def initUpload(self, upload):
msg = 'DICOMweb assetstores are import only.'
raise NotImplementedError(msg)
Expand Down Expand Up @@ -122,9 +132,6 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
:search_filters: (optional) a dictionary of additional search
filters to use with dicomweb_client's `search_for_series()`
function.
:auth: (optional) if the DICOMweb server requires authentication,
this should be an authentication handler derived from
requests.auth.AuthBase.
:type params: dict
:param progress: Object on which to record progress if possible.
Expand All @@ -142,9 +149,9 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
limit = params.get('limit')
search_filters = params.get('search_filters', {})

meta = self.assetstore[DICOMWEB_META_KEY]
meta = self.assetstore_meta

client = _create_dicomweb_client(meta, auth=params.get('auth'))
client = _create_dicomweb_client(meta)

study_uid_key = dicom_key_to_tag('StudyInstanceUID')
series_uid_key = dicom_key_to_tag('SeriesInstanceUID')
Expand Down Expand Up @@ -201,19 +208,37 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
file['imported'] = True
File().save(file)

# FIXME: should we return a list of items (like this), or should
# we return files?
items.append(item)

return items

@property
def auth_session(self):
return _create_auth_session(self.assetstore_meta)


def _create_auth_session(meta):
auth_type = meta.get('auth_type')
if auth_type is None:
return None

if auth_type == 'token':
return _create_token_auth_session(meta['auth_token'])

msg = f'Unhandled auth type: {auth_type}'
raise NotImplementedError(msg)


def _create_token_auth_session(token):
s = requests.Session()
s.headers.update({'Authorization': f'Bearer {token}'})
return s


def _create_dicomweb_client(meta, auth=None):
def _create_dicomweb_client(meta):
from dicomweb_client.api import DICOMwebClient
from dicomweb_client.session_utils import create_session_from_auth

# Create the authentication session
session = create_session_from_auth(auth)
session = _create_auth_session(meta)

# Make the DICOMwebClient
return DICOMwebClient(
Expand Down
5 changes: 2 additions & 3 deletions sources/dicom/large_image_source_dicom/assetstore/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def _importData(self, assetstore, params, progress):
"""
user = self.getCurrentUser()

destinationType = params.get('destinationType', 'folder')
destinationType = params['destinationType']
if destinationType not in ('folder', 'user', 'collection'):
msg = f'Invalid destinationType: {destinationType}'
raise RestException(msg)
Expand Down Expand Up @@ -58,7 +58,6 @@ def _importData(self, assetstore, params, progress):
{
'limit': limit,
'search_filters': search_filters,
'auth': None,
},
progress,
user,
Expand All @@ -77,7 +76,7 @@ def _importData(self, assetstore, params, progress):
'in the Girder data hierarchy under which to import the files.')
.param('destinationType', 'The type of the parent object to import into.',
enum=('folder', 'user', 'collection'),
required=True)
required=False, default='folder')
.param('limit', 'The maximum number of results to import.',
required=False, default=None)
.param('filters', 'Any search parameters to filter DICOM objects.',
Expand Down
5 changes: 4 additions & 1 deletion sources/dicom/large_image_source_dicom/girder_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from girder.models.file import File
from girder.models.folder import Folder
from girder.models.item import Item
from girder.utility import assetstore_utilities

from . import DICOMFileTileSource
from .assetstore import DICOMWEB_META_KEY
Expand Down Expand Up @@ -61,12 +62,14 @@ def _getDICOMwebLargeImagePath(self, assetstore):
file = Item().childFiles(self.item, limit=1)[0]
file_meta = file['dicomweb_meta']

adapter = assetstore_utilities.getAssetstoreAdapter(assetstore)

return {
'url': meta['url'],
'study_uid': file_meta['study_uid'],
'series_uid': file_meta['series_uid'],
# The following are optional
'qido_prefix': meta.get('qido_prefix'),
'wado_prefix': meta.get('wado_prefix'),
'auth': meta.get('auth'),
'session': adapter.auth_session,
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ include dicomwebAssetstoreMixins
input#g-new-dwas-name.input-sm.form-control(
type="text",
placeholder="Name")
+g-dwas-parameters
+g-dwas-parameters("new")
p#g-new-dwas-error.g-validation-failed-message
input.g-new-assetstore-submit.btn.btn-sm.btn-primary(
type="submit", value="Create")
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
include dicomwebAssetstoreMixins

+g-dwas-parameters
+g-dwas-parameters("edit")
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
mixin g-dwas-parameters
mixin g-dwas-parameters(label_key)
- const key = label_key;

//- We need to make sure the html elements all have unique ids when this
//- mixin is reused in different places, so that we can locate the correct
//- html elements in the script.
- const url_id = `g-${key}-dwas-url`;
- const qido_id = `g-${key}-dwas-qido-prefix`;
- const wado_id = `g-${key}-dwas-wado-prefix`;
- const auth_type_id = `g-${key}-dwas-auth-type`;
- const auth_type_container_id = `g-${key}-dwas-auth-type-container`;
- const auth_token_id = `g-${key}-dwas-auth-token`;
- const auth_token_container_id = `g-${key}-dwas-auth-token-container`;

.form-group
label.control-label(for="g-edit-dwas-url") DICOMweb server URL
input#g-edit-dwas-url.input-sm.form-control(
label.control-label(for=url_id) DICOMweb server URL
input.input-sm.form-control(
id=url_id,
type="text",
placeholder="URL")
label.control-label(for="g-edit-dwas-qido-prefix") DICOMweb QIDO prefix (optional)
input#g-edit-dwas-qido-prefix.input-sm.form-control(
label.control-label(for=qido_id) DICOMweb QIDO prefix (optional)
input.input-sm.form-control(
id=qido_id,
type="text",
placeholder="QIDO prefix")
label.control-label(for="g-edit-dwas-wado-prefix") DICOMweb WADO prefix (optional)
input#g-edit-dwas-wado-prefix.input-sm.form-control(
label.control-label(for=wado_id) DICOMweb WADO prefix (optional)
input.input-sm.form-control(
id=wado_id,
type="text",
placeholder="WADO prefix")
//- COMMENTED OUT UNTIL WE ADD AUTHENTICATION
label.control-label(for="g-edit-dwas-auth-type") DICOMweb authentication type (optional)
input#g-edit-dwas-auth-type.input-sm.form-control(
type="text",
placeholder="Authentication type")
label.control-label(for=auth_type_id) DICOMweb authentication type (optional)
- const auth_type = (assetstore && assetstore.attributes.dicomweb_meta.auth_type) || null;
- const updateFuncName = `${key}UpdateVisibilities`;
script.
var #{updateFuncName} = function () {
const isToken = document.getElementById('#{auth_type_id}').value === 'token';
const display = isToken ? 'block' : 'none';
document.getElementById('#{auth_token_container_id}').style.display = display;
};
div(id=auth_type_container_id)
select.form-control(
id=auth_type_id,
onchange=updateFuncName + '()')
each auth_option in authOptions
option(value=auth_option.value, selected=(auth_type === auth_option.value)) #{auth_option.label}
- const display = auth_type === 'token' ? 'block': 'none';
div(id=auth_token_container_id, style='display: ' + display + ';')
label.control-label(for=auth_token_id) DICOMweb authentication token
input.input-sm.form-control(
id=auth_token_id,
type="text",
placeholder="Token")
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const authOptions = [
{
// HTML can't accept null, but it can accept an empty string
value: '',
label: 'None'
},
{
value: 'token',
label: 'Token'
}
];

export default authOptions;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { wrap } from '@girder/core/utilities/PluginUtils';

import DWASEditFieldsTemplate from '../templates/dicomwebAssetstoreEditFields.pug';

import authOptions from './AuthOptions';

/**
* Adds DICOMweb-specific fields to the edit dialog.
*/
Expand All @@ -13,7 +15,8 @@ wrap(EditAssetstoreWidget, 'render', function (render) {
if (this.model.get('type') === AssetstoreType.DICOMWEB) {
this.$('.g-assetstore-form-fields').append(
DWASEditFieldsTemplate({
assetstore: this.model
assetstore: this.model,
authOptions
})
);
}
Expand All @@ -26,14 +29,17 @@ EditAssetstoreWidget.prototype.fieldsMap[AssetstoreType.DICOMWEB] = {
url: this.$('#g-edit-dwas-url').val(),
qido_prefix: this.$('#g-edit-dwas-qido-prefix').val(),
wado_prefix: this.$('#g-edit-dwas-wado-prefix').val(),
auth_type: this.$('#g-edit-dwas-auth-type').val()
auth_type: this.$('#g-edit-dwas-auth-type').val(),
auth_token: this.$('#g-edit-dwas-auth-token').val()
};
},
set: function () {
const dwInfo = this.model.get('dicomweb_meta');
this.$('#g-edit-dwas-url').val(dwInfo.url);
this.$('#g-edit-dwas-qido-prefix').val(dwInfo.qido_prefix);
this.$('#g-edit-dwas-wado-prefix').val(dwInfo.wado_prefix);
this.$('#g-edit-dwas-auth-type').val(dwInfo.auth_type);
// HTML can't accept null, so set it to an empty string
this.$('#g-edit-dwas-auth-type').val(dwInfo.auth_type || '');
this.$('#g-edit-dwas-auth-token').val(dwInfo.auth_token);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@ import { wrap } from '@girder/core/utilities/PluginUtils';

import DWASCreateTemplate from '../templates/dicomwebAssetstoreCreate.pug';

import authOptions from './AuthOptions';

/**
* Add UI for creating new DICOMweb assetstore.
*/
wrap(NewAssetstoreWidget, 'render', function (render) {
render.call(this);

this.$('#g-assetstore-accordion').append(DWASCreateTemplate());
this.$('#g-assetstore-accordion').append(DWASCreateTemplate({
authOptions
}));
return this;
});

NewAssetstoreWidget.prototype.events['submit #g-new-dwas-form'] = function (e) {
this.createAssetstore(e, this.$('#g-new-dwas-error'), {
type: AssetstoreType.DICOMWEB,
name: this.$('#g-new-dwas-name').val(),
url: this.$('#g-edit-dwas-url').val(),
qido_prefix: this.$('#g-edit-dwas-qido-prefix').val(),
wado_prefix: this.$('#g-edit-dwas-wado-prefix').val(),
auth_type: this.$('#g-edit-dwas-auth-type').val()
url: this.$('#g-new-dwas-url').val(),
qido_prefix: this.$('#g-new-dwas-qido-prefix').val(),
wado_prefix: this.$('#g-new-dwas-wado-prefix').val(),
auth_type: this.$('#g-new-dwas-auth-type').val(),
auth_token: this.$('#g-new-dwas-auth-token').val()
});
};
Loading

0 comments on commit 2712dcf

Please sign in to comment.