Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add token authentication option for DICOMweb #1349

Merged
merged 4 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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)

Check warning on line 69 in sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py

View check run for this annotation

Codecov / codecov/patch

sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py#L68-L69

Added lines #L68 - L69 were not covered by tests

# If we found a series, then test the wado prefix as well
if series:
Expand All @@ -76,10 +81,15 @@
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)

Check warning on line 85 in sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py

View check run for this annotation

Codecov / codecov/patch

sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py#L84-L85

Added lines #L84 - L85 were not covered by tests

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 @@
: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 @@
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 @@
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'])

Check warning on line 226 in sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py

View check run for this annotation

Codecov / codecov/patch

sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py#L226

Added line #L226 was not covered by tests

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

Check warning on line 229 in sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py

View check run for this annotation

Codecov / codecov/patch

sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py#L228-L229

Added lines #L228 - L229 were not covered by tests


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

Check warning on line 235 in sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py

View check run for this annotation

Codecov / codecov/patch

sources/dicom/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py#L233-L235

Added lines #L233 - L235 were not covered by tests


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 DWASEditFieldsTemplate from '../templates/dicomwebAssetstoreEditFields.pug';

import authOptions from './AuthOptions';

/**
* Adds DICOMweb-specific fields to the edit dialog.
*/
Expand All @@ -13,7 +15,8 @@
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 @@
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);

Check warning on line 43 in sources/dicom/large_image_source_dicom/web_client/views/EditAssetstoreWidget.js

View check run for this annotation

Codecov / codecov/patch

sources/dicom/large_image_source_dicom/web_client/views/EditAssetstoreWidget.js#L43

Added line #L43 was not covered by tests
}
};
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