Mercurial > hg > orthanc-authorization
comparison Plugin/Plugin.cpp @ 71:30fb3ce960d9
configurable user permissions
author | Alain Mazy <am@osimis.io> |
---|---|
date | Wed, 22 Feb 2023 13:13:38 +0100 |
parents | af44dce56328 |
children | e381ba725669 |
comparison
equal
deleted
inserted
replaced
70:786b202ef24e | 71:30fb3ce960d9 |
---|---|
18 | 18 |
19 #include "AssociativeArray.h" | 19 #include "AssociativeArray.h" |
20 #include "DefaultAuthorizationParser.h" | 20 #include "DefaultAuthorizationParser.h" |
21 #include "CachedAuthorizationService.h" | 21 #include "CachedAuthorizationService.h" |
22 #include "AuthorizationWebService.h" | 22 #include "AuthorizationWebService.h" |
23 #include "PermissionParser.h" | |
23 #include "MemoryCache.h" | 24 #include "MemoryCache.h" |
24 | 25 |
25 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" | 26 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" |
26 | 27 |
27 #include <Compatibility.h> // For std::unique_ptr<> | 28 #include <Compatibility.h> // For std::unique_ptr<> |
28 #include <Logging.h> | 29 #include <Logging.h> |
29 #include <Toolbox.h> | 30 #include <Toolbox.h> |
31 #include <EmbeddedResources.h> | |
30 | 32 |
31 | 33 |
32 // Configuration of the authorization plugin | 34 // Configuration of the authorization plugin |
33 static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_; | 35 static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_; |
34 static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_; | 36 static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_; |
37 static std::unique_ptr<OrthancPlugins::PermissionParser> permissionParser_; | |
35 static std::set<std::string> uncheckedResources_; | 38 static std::set<std::string> uncheckedResources_; |
36 static std::list<std::string> uncheckedFolders_; | 39 static std::list<std::string> uncheckedFolders_; |
37 static std::set<OrthancPlugins::Token> tokens_; | 40 static std::set<OrthancPlugins::Token> tokens_; |
38 static std::set<OrthancPlugins::AccessLevel> uncheckedLevels_; | 41 static std::set<OrthancPlugins::AccessLevel> uncheckedLevels_; |
39 | 42 |
43 | |
44 static std::string JoinStrings(const std::set<std::string>& values) | |
45 { | |
46 std::string out; | |
47 std::set<std::string> copy = values; // TODO: remove after upgrading to OrthancFramework 1.11.3+ | |
48 Orthanc::Toolbox::JoinStrings(out, copy, "|"); | |
49 return out; | |
50 } | |
40 | 51 |
41 static int32_t FilterHttpRequests(OrthancPluginHttpMethod method, | 52 static int32_t FilterHttpRequests(OrthancPluginHttpMethod method, |
42 const char *uri, | 53 const char *uri, |
43 const char *ip, | 54 const char *ip, |
44 uint32_t headersCount, | 55 uint32_t headersCount, |
66 return 1; | 77 return 1; |
67 } | 78 } |
68 } | 79 } |
69 } | 80 } |
70 | 81 |
82 unsigned int validity; // ignored | |
83 | |
84 // check if the user permissions grants him access | |
85 if (permissionParser_.get() != NULL && | |
86 authorizationService_.get() != NULL) | |
87 // && uncheckedLevels_.find(OrthancPlugins::AccessLevel_UserPermissions) == uncheckedLevels_.end()) | |
88 { | |
89 std::set<std::string> requiredPermissions; | |
90 std::string matchedPattern; | |
91 if (permissionParser_->Parse(requiredPermissions, matchedPattern, method, uri)) | |
92 { | |
93 if (tokens_.empty()) | |
94 { | |
95 LOG(INFO) << "Testing whether anonymous user has any of the required permissions '" << JoinStrings(requiredPermissions) << "'"; | |
96 if (authorizationService_->HasAnonymousUserPermission(validity, requiredPermissions)) | |
97 { | |
98 return 1; | |
99 } | |
100 } | |
101 else | |
102 { | |
103 OrthancPlugins::AssociativeArray headers | |
104 (headersCount, headersKeys, headersValues, false); | |
105 | |
106 // Loop over all the authorization tokens stored in the HTTP | |
107 // headers, until finding one that is granted | |
108 for (std::set<OrthancPlugins::Token>::const_iterator | |
109 token = tokens_.begin(); token != tokens_.end(); ++token) | |
110 { | |
111 std::string value; | |
112 | |
113 // we consider that users only works with HTTP Header tokens, not tokens from GetArgument | |
114 if (token->GetType() == OrthancPlugins::TokenType_HttpHeader && | |
115 headers.GetValue(value, token->GetKey())) | |
116 { | |
117 LOG(INFO) << "Testing whether user has the required permission '" << JoinStrings(requiredPermissions) << "' based on the '" << token->GetKey() << "' HTTP header required to match '" << matchedPattern << "'"; | |
118 if (authorizationService_->HasUserPermission(validity, requiredPermissions, *token, value)) | |
119 { | |
120 return 1; | |
121 } | |
122 } | |
123 } | |
124 } | |
125 } | |
126 } | |
127 | |
71 if (authorizationParser_.get() != NULL && | 128 if (authorizationParser_.get() != NULL && |
72 authorizationService_.get() != NULL) | 129 authorizationService_.get() != NULL) |
73 { | 130 { |
74 // Parse the resources that are accessed through this URI | 131 // Parse the resources that are accessed through this URI |
75 OrthancPlugins::IAuthorizationParser::AccessedResources accesses; | 132 OrthancPlugins::IAuthorizationParser::AccessedResources accesses; |
92 LOG(INFO) << "Testing whether access to " | 149 LOG(INFO) << "Testing whether access to " |
93 << OrthancPlugins::EnumerationToString(access->GetLevel()) | 150 << OrthancPlugins::EnumerationToString(access->GetLevel()) |
94 << " \"" << access->GetOrthancId() << "\" is allowed"; | 151 << " \"" << access->GetOrthancId() << "\" is allowed"; |
95 | 152 |
96 bool granted = false; | 153 bool granted = false; |
97 unsigned int validity; // ignored | |
98 | 154 |
99 if (tokens_.empty()) | 155 if (tokens_.empty()) |
100 { | 156 { |
101 granted = authorizationService_->IsGranted(validity, method, *access); | 157 granted = authorizationService_->IsGrantedToAnonymousUser(validity, method, *access); |
102 } | 158 } |
103 else | 159 else |
104 { | 160 { |
105 OrthancPlugins::AssociativeArray headers | 161 OrthancPlugins::AssociativeArray headers |
106 (headersCount, headersKeys, headersValues, false); | 162 (headersCount, headersKeys, headersValues, false); |
191 { | 247 { |
192 try | 248 try |
193 { | 249 { |
194 if (authorizationParser_.get() == NULL) | 250 if (authorizationParser_.get() == NULL) |
195 { | 251 { |
196 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 252 return OrthancPluginErrorCode_Success; |
197 } | 253 } |
198 | 254 |
199 if (changeType == OrthancPluginChangeType_Deleted) | 255 if (changeType == OrthancPluginChangeType_Deleted) |
200 { | 256 { |
201 switch (resourceType) | 257 switch (resourceType) |
283 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | 339 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); |
284 } | 340 } |
285 | 341 |
286 if (hasValue) | 342 if (hasValue) |
287 { | 343 { |
288 authorizationService_->GetUserProfile(profile, *token, value); | 344 unsigned int validity; // not used |
345 authorizationService_->GetUserProfile(validity, profile, *token, value); | |
289 | 346 |
290 OrthancPlugins::AnswerJson(profile, output); | 347 OrthancPlugins::AnswerJson(profile, output); |
291 break; | 348 break; |
292 } | 349 } |
293 } | 350 } |
294 | 351 |
352 } | |
353 } | |
354 | |
355 void MergeJson(Json::Value &a, const Json::Value &b) { | |
356 | |
357 if (!a.isObject() || !b.isObject()) | |
358 { | |
359 return; | |
360 } | |
361 | |
362 Json::Value::Members members = b.getMemberNames(); | |
363 | |
364 for (size_t i = 0; i < members.size(); i++) | |
365 { | |
366 std::string key = members[i]; | |
367 | |
368 if (!a[key].isNull() && a[key].type() == Json::objectValue && b[key].type() == Json::objectValue) | |
369 { | |
370 MergeJson(a[key], b[key]); | |
371 } | |
372 else | |
373 { | |
374 a[key] = b[key]; | |
375 } | |
295 } | 376 } |
296 } | 377 } |
297 | 378 |
298 | 379 |
299 extern "C" | 380 extern "C" |
320 | 401 |
321 OrthancPluginSetDescription(context, "Advanced authorization plugin for Orthanc."); | 402 OrthancPluginSetDescription(context, "Advanced authorization plugin for Orthanc."); |
322 | 403 |
323 try | 404 try |
324 { | 405 { |
325 OrthancPlugins::OrthancConfiguration general; | 406 static const char* PLUGIN_SECTION = "Authorization"; |
326 | 407 |
327 static const char* SECTION = "Authorization"; | 408 OrthancPlugins::OrthancConfiguration orthancFullConfiguration; |
328 if (general.IsSection(SECTION)) | 409 |
329 { | 410 // read default configuration |
330 OrthancPlugins::OrthancConfiguration configuration; | 411 std::string defaultConfigurationFileContent; |
331 general.GetSection(configuration, "Authorization"); | 412 Orthanc::EmbeddedResources::GetFileResource(defaultConfigurationFileContent, Orthanc::EmbeddedResources::DEFAULT_CONFIGURATION); |
413 Json::Value pluginJsonDefaultConfiguration; | |
414 OrthancPlugins::ReadJsonWithoutComments(pluginJsonDefaultConfiguration, defaultConfigurationFileContent); | |
415 Json::Value pluginJsonConfiguration = pluginJsonDefaultConfiguration[PLUGIN_SECTION]; | |
416 | |
417 OrthancPlugins::OrthancConfiguration pluginProvidedConfiguration; | |
418 | |
419 if (orthancFullConfiguration.IsSection(PLUGIN_SECTION)) | |
420 { | |
421 // get the configuration provided by the user | |
422 orthancFullConfiguration.GetSection(pluginProvidedConfiguration, PLUGIN_SECTION); | |
423 | |
424 // merge it with the default configuration. This is a way to apply the all default values in a single step | |
425 MergeJson(pluginJsonConfiguration, pluginProvidedConfiguration.GetJson()); | |
426 | |
427 // recreate a OrthancConfiguration object from the merged configuration | |
428 OrthancPlugins::OrthancConfiguration pluginConfiguration(pluginJsonConfiguration, PLUGIN_SECTION); | |
332 | 429 |
333 // TODO - The size of the caches is set to 10,000 items. Maybe add a configuration option? | 430 // TODO - The size of the caches is set to 10,000 items. Maybe add a configuration option? |
334 OrthancPlugins::MemoryCache::Factory factory(10000); | 431 OrthancPlugins::MemoryCache::Factory factory(10000); |
335 | 432 |
336 { | 433 std::string dicomWebRoot = "/dicom-web/"; |
337 std::string root; | 434 std::string oe2Root = "/ui/"; |
338 | 435 |
339 if (configuration.IsSection("DicomWeb")) | 436 if (orthancFullConfiguration.IsSection("DicomWeb")) |
340 { | 437 { |
341 OrthancPlugins::OrthancConfiguration dicomWeb; | 438 OrthancPlugins::OrthancConfiguration dicomWeb; |
342 dicomWeb.GetSection(configuration, "DicomWeb"); | 439 dicomWeb.GetSection(orthancFullConfiguration, "DicomWeb"); |
343 root = dicomWeb.GetStringValue("Root", ""); | 440 dicomWebRoot = dicomWeb.GetStringValue("Root", "/dicom-web/"); |
344 } | 441 } |
345 | 442 |
346 if (root.empty()) | 443 if (orthancFullConfiguration.IsSection("OrthancExplorer2")) |
347 { | 444 { |
348 root = "/dicom-web/"; | 445 OrthancPlugins::OrthancConfiguration oe2; |
349 } | 446 oe2.GetSection(orthancFullConfiguration, "OrthancExplorer2"); |
350 | 447 oe2Root = oe2.GetStringValue("Root", "/ui/"); |
351 authorizationParser_.reset | |
352 (new OrthancPlugins::DefaultAuthorizationParser(factory, root)); | |
353 } | 448 } |
354 | 449 |
355 std::list<std::string> tmp; | 450 std::list<std::string> tmp; |
356 | 451 |
357 configuration.LookupListOfStrings(tmp, "TokenHttpHeaders", true); | 452 pluginConfiguration.LookupListOfStrings(tmp, "TokenHttpHeaders", true); |
358 for (std::list<std::string>::const_iterator | 453 for (std::list<std::string>::const_iterator |
359 it = tmp.begin(); it != tmp.end(); ++it) | 454 it = tmp.begin(); it != tmp.end(); ++it) |
360 { | 455 { |
361 tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, *it)); | 456 tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, *it)); |
362 } | 457 } |
363 | 458 |
364 configuration.LookupListOfStrings(tmp, "TokenGetArguments", true); | 459 pluginConfiguration.LookupListOfStrings(tmp, "TokenGetArguments", true); |
365 | 460 |
366 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 0) | 461 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 0) |
367 for (std::list<std::string>::const_iterator | 462 for (std::list<std::string>::const_iterator |
368 it = tmp.begin(); it != tmp.end(); ++it) | 463 it = tmp.begin(); it != tmp.end(); ++it) |
369 { | 464 { |
377 "The option \"TokenGetArguments\" of the authorization plugin " | 472 "The option \"TokenGetArguments\" of the authorization plugin " |
378 "is only valid if compiled against Orthanc >= 1.3.0" | 473 "is only valid if compiled against Orthanc >= 1.3.0" |
379 } | 474 } |
380 #endif | 475 #endif |
381 | 476 |
382 configuration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false); | 477 pluginConfiguration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false); |
383 configuration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false); | 478 pluginConfiguration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false); |
384 | 479 |
385 std::string url; | 480 std::string url; |
386 | 481 |
387 static const char* WEB_SERVICE = "WebService"; | 482 static const char* WEB_SERVICE = "WebService"; |
388 if (!configuration.LookupStringValue(url, WEB_SERVICE)) | 483 if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE)) |
389 { | 484 { |
390 throw Orthanc::OrthancException( | 485 LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE << "\" configuration provided. Will not perform resource based authorization."; |
391 Orthanc::ErrorCode_BadFileFormat, | 486 } |
392 "Missing mandatory option \"" + std::string(WEB_SERVICE) + | 487 else |
393 "\" for the authorization plugin"); | 488 { |
489 authorizationParser_.reset | |
490 (new OrthancPlugins::DefaultAuthorizationParser(factory, dicomWebRoot)); | |
491 } | |
492 | |
493 static const char* WEB_SERVICE_USER_PROFILE = "WebServiceUserProfileUrl"; | |
494 static const char* PERMISSIONS = "Permissions"; | |
495 if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE_USER_PROFILE)) | |
496 { | |
497 LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE_USER_PROFILE << "\" configuration provided. Will not perform user-permissions based authorization."; | |
498 } | |
499 else | |
500 { | |
501 if (!pluginConfiguration.GetJson().isMember(PERMISSIONS)) | |
502 { | |
503 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing required \"" + std::string(PERMISSIONS) + | |
504 "\" option since you have defined the \"" + std::string(WEB_SERVICE_USER_PROFILE) + "\" option"); | |
505 } | |
506 permissionParser_.reset | |
507 (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root)); | |
508 | |
509 permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS]); | |
510 } | |
511 | |
512 if (authorizationParser_.get() == NULL && permissionParser_.get() == NULL) | |
513 { | |
514 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing one of the mandatory option \"" + std::string(WEB_SERVICE) + | |
515 "\" or \"" + std::string(WEB_SERVICE_USER_PROFILE) + "\""); | |
394 } | 516 } |
395 | 517 |
396 std::set<std::string> standardConfigurations; | 518 std::set<std::string> standardConfigurations; |
397 if (configuration.LookupSetOfStrings(standardConfigurations, "StandardConfigurations", false)) | 519 if (pluginConfiguration.LookupSetOfStrings(standardConfigurations, "StandardConfigurations", false)) |
398 { | 520 { |
399 if (standardConfigurations.find("osimis-web-viewer") != standardConfigurations.end()) | 521 if (standardConfigurations.find("osimis-web-viewer") != standardConfigurations.end()) |
400 { | 522 { |
401 uncheckedFolders_.push_back("/osimis-viewer/app/"); | 523 uncheckedFolders_.push_back("/osimis-viewer/app/"); |
402 uncheckedFolders_.push_back("/osimis-viewer/languages/"); | 524 uncheckedFolders_.push_back("/osimis-viewer/languages/"); |
417 | 539 |
418 if (standardConfigurations.find("orthanc-explorer-2") != standardConfigurations.end()) | 540 if (standardConfigurations.find("orthanc-explorer-2") != standardConfigurations.end()) |
419 { | 541 { |
420 uncheckedFolders_.push_back("/ui/app/"); | 542 uncheckedFolders_.push_back("/ui/app/"); |
421 uncheckedResources_.insert("/ui/api/pre-login-configuration"); // for the UI to know, i.e. if Keycloak is enabled or not | 543 uncheckedResources_.insert("/ui/api/pre-login-configuration"); // for the UI to know, i.e. if Keycloak is enabled or not |
544 uncheckedResources_.insert("/ui/api/configuration"); | |
422 uncheckedResources_.insert("/auth/user-profile"); | 545 uncheckedResources_.insert("/auth/user-profile"); |
423 | 546 |
424 tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, "Authorization")); // for basic-auth | 547 tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, "Authorization")); // for basic-auth |
425 tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, "token")); // for keycloak | 548 tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, "token")); // for keycloak |
426 } | 549 } |
427 | 550 |
428 } | 551 } |
429 | 552 |
430 std::string checkedLevelString; | 553 std::string checkedLevelString; |
431 if (configuration.LookupStringValue(checkedLevelString, "CheckedLevel")) | 554 if (pluginConfiguration.LookupStringValue(checkedLevelString, "CheckedLevel")) |
432 { | 555 { |
433 OrthancPlugins::AccessLevel checkedLevel = OrthancPlugins::StringToAccessLevel(checkedLevelString); | 556 OrthancPlugins::AccessLevel checkedLevel = OrthancPlugins::StringToAccessLevel(checkedLevelString); |
434 if (checkedLevel == OrthancPlugins::AccessLevel_Instance) | 557 if (checkedLevel == OrthancPlugins::AccessLevel_Instance) |
435 { | 558 { |
436 uncheckedLevels_.insert(OrthancPlugins::AccessLevel_Patient); | 559 uncheckedLevels_.insert(OrthancPlugins::AccessLevel_Patient); |
455 uncheckedLevels_.insert(OrthancPlugins::AccessLevel_Series); | 578 uncheckedLevels_.insert(OrthancPlugins::AccessLevel_Series); |
456 uncheckedLevels_.insert(OrthancPlugins::AccessLevel_Instance); | 579 uncheckedLevels_.insert(OrthancPlugins::AccessLevel_Instance); |
457 } | 580 } |
458 } | 581 } |
459 | 582 |
460 if (configuration.LookupListOfStrings(tmp, "UncheckedLevels", false)) | 583 if (pluginConfiguration.LookupListOfStrings(tmp, "UncheckedLevels", false)) |
461 { | 584 { |
462 if (uncheckedLevels_.size() == 0) | 585 if (uncheckedLevels_.size() == 0) |
463 { | 586 { |
464 for (std::list<std::string>::const_iterator | 587 for (std::list<std::string>::const_iterator |
465 it = tmp.begin(); it != tmp.end(); ++it) | 588 it = tmp.begin(); it != tmp.end(); ++it) |
475 } | 598 } |
476 | 599 |
477 std::unique_ptr<OrthancPlugins::AuthorizationWebService> webService(new OrthancPlugins::AuthorizationWebService(url)); | 600 std::unique_ptr<OrthancPlugins::AuthorizationWebService> webService(new OrthancPlugins::AuthorizationWebService(url)); |
478 | 601 |
479 std::string webServiceIdentifier; | 602 std::string webServiceIdentifier; |
480 if (configuration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier")) | 603 if (pluginConfiguration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier")) |
481 { | 604 { |
482 webService->SetIdentifier(webServiceIdentifier); | 605 webService->SetIdentifier(webServiceIdentifier); |
483 } | 606 } |
484 | 607 |
485 std::string webServiceUsername; | 608 std::string webServiceUsername; |
486 std::string webServicePassword; | 609 std::string webServicePassword; |
487 if (configuration.LookupStringValue(webServiceUsername, "WebServiceUsername") && configuration.LookupStringValue(webServicePassword, "WebServicePassword")) | 610 if (pluginConfiguration.LookupStringValue(webServiceUsername, "WebServiceUsername") && pluginConfiguration.LookupStringValue(webServicePassword, "WebServicePassword")) |
488 { | 611 { |
489 webService->SetCredentials(webServiceUsername, webServicePassword); | 612 webService->SetCredentials(webServiceUsername, webServicePassword); |
490 } | 613 } |
491 | 614 |
492 std::string webServiceUserProfileUrl; | 615 std::string webServiceUserProfileUrl; |
493 if (configuration.LookupStringValue(webServiceUserProfileUrl, "WebServiceUserProfileUrl")) | 616 if (pluginConfiguration.LookupStringValue(webServiceUserProfileUrl, "WebServiceUserProfileUrl")) |
494 { | 617 { |
495 webService->SetUserProfileUrl(webServiceUserProfileUrl); | 618 webService->SetUserProfileUrl(webServiceUserProfileUrl); |
496 } | 619 } |
497 | 620 |
498 authorizationService_.reset | 621 authorizationService_.reset |
508 OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterHttpRequestsFallback); | 631 OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterHttpRequestsFallback); |
509 #endif | 632 #endif |
510 } | 633 } |
511 else | 634 else |
512 { | 635 { |
513 LOG(WARNING) << "No section \"" << SECTION << "\" in the configuration file, " | 636 LOG(WARNING) << "No section \"" << PLUGIN_SECTION << "\" in the configuration file, " |
514 << "the authorization plugin is disabled"; | 637 << "the authorization plugin is disabled"; |
515 } | 638 } |
516 } | 639 } |
517 catch (Orthanc::OrthancException& e) | 640 catch (Orthanc::OrthancException& e) |
518 { | 641 { |