comparison Plugin/Plugin.cpp @ 72:e381ba725669

new PUT auth/tokens/{token-type} API route + updated interface with WebService
author Alain Mazy <am@osimis.io>
date Fri, 24 Feb 2023 18:13:36 +0100
parents 30fb3ce960d9
children 512247750f0a
comparison
equal deleted inserted replaced
71:30fb3ce960d9 72:e381ba725669
47 std::set<std::string> copy = values; // TODO: remove after upgrading to OrthancFramework 1.11.3+ 47 std::set<std::string> copy = values; // TODO: remove after upgrading to OrthancFramework 1.11.3+
48 Orthanc::Toolbox::JoinStrings(out, copy, "|"); 48 Orthanc::Toolbox::JoinStrings(out, copy, "|");
49 return out; 49 return out;
50 } 50 }
51 51
52 struct TokenAndValue
53 {
54 const OrthancPlugins::Token& token;
55 std::string value;
56
57 TokenAndValue(const OrthancPlugins::Token& token, const std::string& value) :
58 token(token),
59 value(value)
60 {
61 }
62 };
63
64
52 static int32_t FilterHttpRequests(OrthancPluginHttpMethod method, 65 static int32_t FilterHttpRequests(OrthancPluginHttpMethod method,
53 const char *uri, 66 const char *uri,
54 const char *ip, 67 const char *ip,
55 uint32_t headersCount, 68 uint32_t headersCount,
56 const char *const *headersKeys, 69 const char *const *headersKeys,
59 const char *const *getArgumentsKeys, 72 const char *const *getArgumentsKeys,
60 const char *const *getArgumentsValues) 73 const char *const *getArgumentsValues)
61 { 74 {
62 try 75 try
63 { 76 {
77 unsigned int validity; // ignored
78
64 if (method == OrthancPluginHttpMethod_Get) 79 if (method == OrthancPluginHttpMethod_Get)
65 { 80 {
66 // Allow GET accesses to static resources 81 // Allow GET accesses to static resources
67 if (uncheckedResources_.find(uri) != uncheckedResources_.end()) 82 if (uncheckedResources_.find(uri) != uncheckedResources_.end())
68 { 83 {
77 return 1; 92 return 1;
78 } 93 }
79 } 94 }
80 } 95 }
81 96
82 unsigned int validity; // ignored 97 OrthancPlugins::AssociativeArray headers(headersCount, headersKeys, headersValues, false);
98 OrthancPlugins::AssociativeArray getArguments(getArgumentsCount, getArgumentsKeys, getArgumentsValues, true);
99
100 std::vector<TokenAndValue> authTokens; // the tokens that are set in this request
101
102 for (std::set<OrthancPlugins::Token>::const_iterator token = tokens_.begin(); token != tokens_.end(); ++token)
103 {
104 std::string value;
105
106 bool hasValue = false;
107 switch (token->GetType())
108 {
109 case OrthancPlugins::TokenType_HttpHeader:
110 hasValue = headers.GetValue(value, token->GetKey());
111 break;
112
113 case OrthancPlugins::TokenType_GetArgument:
114 hasValue = getArguments.GetValue(value, token->GetKey());
115 break;
116
117 default:
118 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
119 }
120
121 if (hasValue)
122 {
123 authTokens.push_back(TokenAndValue(*token, value));
124 }
125 }
83 126
84 // check if the user permissions grants him access 127 // check if the user permissions grants him access
85 if (permissionParser_.get() != NULL && 128 if (permissionParser_.get() != NULL &&
86 authorizationService_.get() != NULL) 129 authorizationService_.get() != NULL)
87 // && uncheckedLevels_.find(OrthancPlugins::AccessLevel_UserPermissions) == uncheckedLevels_.end()) 130 // && uncheckedLevels_.find(OrthancPlugins::AccessLevel_UserPermissions) == uncheckedLevels_.end())
88 { 131 {
89 std::set<std::string> requiredPermissions; 132 std::set<std::string> requiredPermissions;
90 std::string matchedPattern; 133 std::string matchedPattern;
91 if (permissionParser_->Parse(requiredPermissions, matchedPattern, method, uri)) 134 if (permissionParser_->Parse(requiredPermissions, matchedPattern, method, uri))
92 { 135 {
93 if (tokens_.empty()) 136 if (authTokens.empty())
94 { 137 {
95 LOG(INFO) << "Testing whether anonymous user has any of the required permissions '" << JoinStrings(requiredPermissions) << "'"; 138 LOG(INFO) << "Testing whether anonymous user has any of the required permissions '" << JoinStrings(requiredPermissions) << "'";
96 if (authorizationService_->HasAnonymousUserPermission(validity, requiredPermissions)) 139 if (authorizationService_->HasAnonymousUserPermission(validity, requiredPermissions))
97 { 140 {
98 return 1; 141 return 1;
99 } 142 }
100 } 143 }
101 else 144 else
102 { 145 {
103 OrthancPlugins::AssociativeArray headers 146 for (size_t i = 0; i < authTokens.size(); ++i)
104 (headersCount, headersKeys, headersValues, false); 147 {
105 148 LOG(INFO) << "Testing whether user has the required permission '" << JoinStrings(requiredPermissions) << "' based on the '" << authTokens[i].token.GetKey() << "' HTTP header required to match '" << matchedPattern << "'";
106 // Loop over all the authorization tokens stored in the HTTP 149 if (authorizationService_->HasUserPermission(validity, requiredPermissions, authTokens[i].token, authTokens[i].value))
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 { 150 {
117 LOG(INFO) << "Testing whether user has the required permission '" << JoinStrings(requiredPermissions) << "' based on the '" << token->GetKey() << "' HTTP header required to match '" << matchedPattern << "'"; 151 return 1;
118 if (authorizationService_->HasUserPermission(validity, requiredPermissions, *token, value))
119 {
120 return 1;
121 }
122 } 152 }
123 } 153 }
124 } 154 }
125 } 155 }
126 } 156 }
128 if (authorizationParser_.get() != NULL && 158 if (authorizationParser_.get() != NULL &&
129 authorizationService_.get() != NULL) 159 authorizationService_.get() != NULL)
130 { 160 {
131 // Parse the resources that are accessed through this URI 161 // Parse the resources that are accessed through this URI
132 OrthancPlugins::IAuthorizationParser::AccessedResources accesses; 162 OrthancPlugins::IAuthorizationParser::AccessedResources accesses;
133 OrthancPlugins::AssociativeArray getArguments(getArgumentsCount, getArgumentsKeys, getArgumentsValues, true);
134 163
135 if (!authorizationParser_->Parse(accesses, uri, getArguments.GetMap())) 164 if (!authorizationParser_->Parse(accesses, uri, getArguments.GetMap()))
136 { 165 {
137 return 0; // Unable to parse this URI 166 return 0; // Unable to parse this URI
138 } 167 }
150 << OrthancPlugins::EnumerationToString(access->GetLevel()) 179 << OrthancPlugins::EnumerationToString(access->GetLevel())
151 << " \"" << access->GetOrthancId() << "\" is allowed"; 180 << " \"" << access->GetOrthancId() << "\" is allowed";
152 181
153 bool granted = false; 182 bool granted = false;
154 183
155 if (tokens_.empty()) 184 if (authTokens.empty())
156 { 185 {
157 granted = authorizationService_->IsGrantedToAnonymousUser(validity, method, *access); 186 granted = authorizationService_->IsGrantedToAnonymousUser(validity, method, *access);
158 } 187 }
159 else 188 else
160 { 189 {
161 OrthancPlugins::AssociativeArray headers 190 // Loop over all the authorization tokens in the request until finding one that is granted
162 (headersCount, headersKeys, headersValues, false); 191 for (size_t i = 0; i < authTokens.size(); ++i)
163
164 // Loop over all the authorization tokens stored in the HTTP
165 // headers, until finding one that is granted
166 for (std::set<OrthancPlugins::Token>::const_iterator
167 token = tokens_.begin(); token != tokens_.end(); ++token)
168 { 192 {
169 std::string value; 193 if (authorizationService_->IsGranted(validity, method, *access, authTokens[i].token, authTokens[i].value))
170
171 bool hasValue = false;
172 switch (token->GetType())
173 {
174 case OrthancPlugins::TokenType_HttpHeader:
175 hasValue = headers.GetValue(value, token->GetKey());
176 break;
177
178 case OrthancPlugins::TokenType_GetArgument:
179 hasValue = getArguments.GetValue(value, token->GetKey());
180 break;
181
182 default:
183 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
184 }
185
186 if (hasValue &&
187 authorizationService_->IsGranted(validity, method, *access, *token, value))
188 { 194 {
189 granted = true; 195 granted = true;
190 break; 196 break;
191 } 197 }
192 } 198 }
294 LOG(ERROR) << "Unhandled internal exception"; 300 LOG(ERROR) << "Unhandled internal exception";
295 return OrthancPluginErrorCode_Success; // Ignore error 301 return OrthancPluginErrorCode_Success; // Ignore error
296 } 302 }
297 } 303 }
298 304
305 void CreateToken(OrthancPluginRestOutput* output,
306 const char* /*url*/,
307 const OrthancPluginHttpRequest* request)
308 {
309 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
310
311 if (request->method != OrthancPluginHttpMethod_Put)
312 {
313 OrthancPluginSendMethodNotAllowed(context, output, "PUT");
314 }
315 else
316 {
317 // The filtering to this route is performed by this plugin as it is done for any other route before we get here.
318 // Since the route contains the tokenType, we can allow/forbid creating them based on the url
319
320 // simply forward the request to the auth-service
321 std::string tokenType;
322 if (request->groupsCount == 1)
323 {
324 tokenType = request->groups[0];
325 }
326 else
327 {
328 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
329 }
330
331 // convert from Orthanc flavored API to WebService API
332 Json::Value body;
333 if (!OrthancPlugins::ReadJson(body, request->body, request->bodySize))
334 {
335 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected");
336 }
337
338 std::string id;
339 std::vector<OrthancPlugins::IAuthorizationService::OrthancResource> resources;
340 std::string expirationDateString;
341
342 if (body.isMember("ID"))
343 {
344 id = body["ID"].asString();
345 }
346
347 for (Json::ArrayIndex i = 0; i < body["Resources"].size(); ++i)
348 {
349 const Json::Value& jsonResource = body["Resources"][i];
350 OrthancPlugins::IAuthorizationService::OrthancResource resource;
351
352 if (jsonResource.isMember("DicomUid"))
353 {
354 resource.dicomUid = jsonResource["DicomUid"].asString();
355 }
356
357 if (jsonResource.isMember("OrthancId"))
358 {
359 resource.orthancId = jsonResource["OrthancId"].asString();
360 }
361
362 if (jsonResource.isMember("Url"))
363 {
364 resource.url = jsonResource["Url"].asString();
365 }
366
367 resource.level = jsonResource["Level"].asString();
368 resources.push_back(resource);
369 }
370
371 if (body.isMember("ExpirationDate"))
372 {
373 expirationDateString = body["ExpirationDate"].asString();
374 }
375
376 OrthancPlugins::IAuthorizationService::CreatedToken createdToken;
377 if (authorizationService_->CreateToken(createdToken,
378 tokenType,
379 id,
380 resources,
381 expirationDateString))
382 {
383 Json::Value createdJsonToken;
384 createdJsonToken["Token"] = createdToken.token;
385
386 if (!createdToken.url.empty())
387 {
388 createdJsonToken["Url"] = createdToken.url;
389 }
390 else
391 {
392 createdJsonToken["Url"] = Json::nullValue;
393 }
394
395 OrthancPlugins::AnswerJson(createdJsonToken, output);
396 }
397
398
399 }
400 }
401
299 void GetUserProfile(OrthancPluginRestOutput* output, 402 void GetUserProfile(OrthancPluginRestOutput* output,
300 const char* /*url*/, 403 const char* /*url*/,
301 const OrthancPluginHttpRequest* request) 404 const OrthancPluginHttpRequest* request)
302 { 405 {
303 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); 406 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
311 OrthancPlugins::AssociativeArray headers 414 OrthancPlugins::AssociativeArray headers
312 (request->headersCount, request->headersKeys, request->headersValues, false); 415 (request->headersCount, request->headersKeys, request->headersValues, false);
313 416
314 OrthancPlugins::AssociativeArray getArguments 417 OrthancPlugins::AssociativeArray getArguments
315 (request->getCount, request->getKeys, request->getValues, true); 418 (request->getCount, request->getKeys, request->getValues, true);
316
317 419
318 // Loop over all the authorization tokens stored in the HTTP 420 // Loop over all the authorization tokens stored in the HTTP
319 // headers, until finding one that is granted 421 // headers, until finding one that is granted
320 for (std::set<OrthancPlugins::Token>::const_iterator 422 for (std::set<OrthancPlugins::Token>::const_iterator
321 token = tokens_.begin(); token != tokens_.end(); ++token) 423 token = tokens_.begin(); token != tokens_.end(); ++token)
475 #endif 577 #endif
476 578
477 pluginConfiguration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false); 579 pluginConfiguration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false);
478 pluginConfiguration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false); 580 pluginConfiguration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false);
479 581
480 std::string url; 582 std::string urlTokenValidation;
481 583 std::string urlTokenCreationBase;
482 static const char* WEB_SERVICE = "WebService"; 584 std::string urlUserProfile;
483 if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE)) 585 std::string urlRoot;
484 { 586
485 LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE << "\" configuration provided. Will not perform resource based authorization."; 587 static const char* WEB_SERVICE_ROOT = "WebServiceRootUrl";
486 } 588 static const char* WEB_SERVICE_TOKEN_VALIDATION = "WebServiceTokenValidationUrl";
487 else 589 static const char* WEB_SERVICE_TOKEN_CREATION_BASE = "WebServiceTokenCreationBaseUrl";
488 { 590 static const char* WEB_SERVICE_USER_PROFILE = "WebServiceUserProfileUrl";
591 static const char* WEB_SERVICE_TOKEN_VALIDATION_LEGACY = "WebService";
592 if (pluginConfiguration.LookupStringValue(urlRoot, WEB_SERVICE_ROOT))
593 {
594 urlTokenValidation = Orthanc::Toolbox::JoinUri(urlRoot, "/tokens/validate");
595 urlTokenCreationBase = Orthanc::Toolbox::JoinUri(urlRoot, "/tokens/");
596 urlUserProfile = Orthanc::Toolbox::JoinUri(urlRoot, "/user/get-profile");
597 }
598 else
599 {
600 pluginConfiguration.LookupStringValue(urlTokenValidation, WEB_SERVICE_TOKEN_VALIDATION);
601 if (urlTokenValidation.empty())
602 {
603 pluginConfiguration.LookupStringValue(urlTokenValidation, WEB_SERVICE_TOKEN_VALIDATION_LEGACY);
604 }
605
606 pluginConfiguration.LookupStringValue(urlTokenCreationBase, WEB_SERVICE_TOKEN_CREATION_BASE);
607 pluginConfiguration.LookupStringValue(urlUserProfile, WEB_SERVICE_USER_PROFILE);
608 }
609
610 if (!urlTokenValidation.empty())
611 {
612 LOG(WARNING) << "Authorization plugin: url defined for Token Validation: " << urlTokenValidation;
489 authorizationParser_.reset 613 authorizationParser_.reset
490 (new OrthancPlugins::DefaultAuthorizationParser(factory, dicomWebRoot)); 614 (new OrthancPlugins::DefaultAuthorizationParser(factory, dicomWebRoot));
491 } 615 }
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 616 else
500 { 617 {
618 LOG(WARNING) << "Authorization plugin: no url defined for Token Validation";
619 }
620
621 if (!urlUserProfile.empty())
622 {
623 LOG(WARNING) << "Authorization plugin: url defined for User Profile: " << urlUserProfile;
624
625 static const char* PERMISSIONS = "Permissions";
501 if (!pluginConfiguration.GetJson().isMember(PERMISSIONS)) 626 if (!pluginConfiguration.GetJson().isMember(PERMISSIONS))
502 { 627 {
503 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing required \"" + std::string(PERMISSIONS) + 628 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"); 629 "\" option since you have defined the \"" + std::string(WEB_SERVICE_ROOT) + "\" option");
505 } 630 }
506 permissionParser_.reset 631 permissionParser_.reset
507 (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root)); 632 (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root));
508 633
509 permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS]); 634 permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS]);
510 } 635 }
636 else
637 {
638 LOG(WARNING) << "Authorization plugin: no url defined for User Profile";
639 }
640
641 if (!urlTokenCreationBase.empty())
642 {
643 LOG(WARNING) << "Authorization plugin: base url defined for Token Creation : " << urlTokenCreationBase;
644 // TODO Token Creation
645 }
646 else
647 {
648 LOG(WARNING) << "Authorization plugin: no base url defined for Token Creation";
649 }
511 650
512 if (authorizationParser_.get() == NULL && permissionParser_.get() == NULL) 651 if (authorizationParser_.get() == NULL && permissionParser_.get() == NULL)
513 { 652 {
514 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing one of the mandatory option \"" + std::string(WEB_SERVICE) + 653 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: No Token Validation or User Profile url defined");
515 "\" or \"" + std::string(WEB_SERVICE_USER_PROFILE) + "\"");
516 } 654 }
517 655
518 std::set<std::string> standardConfigurations; 656 std::set<std::string> standardConfigurations;
519 if (pluginConfiguration.LookupSetOfStrings(standardConfigurations, "StandardConfigurations", false)) 657 if (pluginConfiguration.LookupSetOfStrings(standardConfigurations, "StandardConfigurations", false))
520 { 658 {
595 LOG(ERROR) << "Authorization plugin: you may only provide one of 'CheckedLevel' or 'UncheckedLevels' configurations"; 733 LOG(ERROR) << "Authorization plugin: you may only provide one of 'CheckedLevel' or 'UncheckedLevels' configurations";
596 return -1; 734 return -1;
597 } 735 }
598 } 736 }
599 737
600 std::unique_ptr<OrthancPlugins::AuthorizationWebService> webService(new OrthancPlugins::AuthorizationWebService(url)); 738 std::unique_ptr<OrthancPlugins::AuthorizationWebService> webService(new OrthancPlugins::AuthorizationWebService(urlTokenValidation,
739 urlTokenCreationBase,
740 urlUserProfile));
601 741
602 std::string webServiceIdentifier; 742 std::string webServiceIdentifier;
603 if (pluginConfiguration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier")) 743 if (pluginConfiguration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier"))
604 { 744 {
605 webService->SetIdentifier(webServiceIdentifier); 745 webService->SetIdentifier(webServiceIdentifier);
610 if (pluginConfiguration.LookupStringValue(webServiceUsername, "WebServiceUsername") && pluginConfiguration.LookupStringValue(webServicePassword, "WebServicePassword")) 750 if (pluginConfiguration.LookupStringValue(webServiceUsername, "WebServiceUsername") && pluginConfiguration.LookupStringValue(webServicePassword, "WebServicePassword"))
611 { 751 {
612 webService->SetCredentials(webServiceUsername, webServicePassword); 752 webService->SetCredentials(webServiceUsername, webServicePassword);
613 } 753 }
614 754
615 std::string webServiceUserProfileUrl;
616 if (pluginConfiguration.LookupStringValue(webServiceUserProfileUrl, "WebServiceUserProfileUrl"))
617 {
618 webService->SetUserProfileUrl(webServiceUserProfileUrl);
619 }
620
621 authorizationService_.reset 755 authorizationService_.reset
622 (new OrthancPlugins::CachedAuthorizationService 756 (new OrthancPlugins::CachedAuthorizationService
623 (webService.release(), factory)); 757 (webService.release(), factory));
624 758
625 OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); 759 if (!urlTokenValidation.empty())
626 OrthancPlugins::RegisterRestCallback<GetUserProfile>("/auth/user-profile", true); 760 {
761 OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
762 }
763
764 if (!urlUserProfile.empty())
765 {
766 OrthancPlugins::RegisterRestCallback<GetUserProfile>("/auth/user/profile", true);
767 }
768
769 if (!urlTokenCreationBase.empty())
770 {
771 OrthancPlugins::RegisterRestCallback<CreateToken>("/auth/tokens/(.*)", true);
772 }
627 773
628 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 1) 774 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 1)
629 OrthancPluginRegisterIncomingHttpRequestFilter2(context, FilterHttpRequests); 775 OrthancPluginRegisterIncomingHttpRequestFilter2(context, FilterHttpRequests);
630 #else 776 #else
631 OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterHttpRequestsFallback); 777 OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterHttpRequestsFallback);