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 {