# HG changeset patch # User Alain Mazy # Date 1693411809 -7200 # Node ID 2b1a95c7d26347a04606fcf72db9b32087d65d97 # Parent aa56dcf599b975e5a09f425cd0c67e988c4b5c77 wip: adjust tools/find queries diff -r aa56dcf599b9 -r 2b1a95c7d263 Plugin/Plugin.cpp --- a/Plugin/Plugin.cpp Tue Aug 22 17:50:01 2023 +0200 +++ b/Plugin/Plugin.cpp Wed Aug 30 18:10:09 2023 +0200 @@ -27,6 +27,7 @@ #include // For std::unique_ptr<> #include #include +#include #include @@ -340,6 +341,193 @@ } } + +bool GetUserProfileInternal(OrthancPlugins::IAuthorizationService::UserProfile& profile, const OrthancPluginHttpRequest* request) +{ + OrthancPlugins::AssociativeArray headers + (request->headersCount, request->headersKeys, request->headersValues, false); + + OrthancPlugins::AssociativeArray getArguments + (request->getCount, request->getKeys, request->getValues, true); + + // Loop over all the authorization tokens stored in the HTTP + // headers, until finding one that is granted + for (std::set::const_iterator + token = tokens_.begin(); token != tokens_.end(); ++token) + { + OrthancPlugins::IAuthorizationService::UserProfile tryProfile; + + std::string value; + + bool hasValue = false; + switch (token->GetType()) + { + case OrthancPlugins::TokenType_HttpHeader: + hasValue = headers.GetValue(value, token->GetKey()); + break; + + case OrthancPlugins::TokenType_GetArgument: + hasValue = getArguments.GetValue(value, token->GetKey()); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (hasValue) + { + unsigned int validity; // not used + if (authorizationService_->GetUserProfile(validity, tryProfile, *token, value)) + { + profile = tryProfile; + return true; + } + } + } + + return false; +} + + +void AdjustToolsFindQueryLabels(Json::Value& query, const OrthancPlugins::IAuthorizationService::UserProfile& profile) +{ + std::set labelsToFind; + std::string labelsConstraint = "Invalid"; + + if (query.isMember("Labels") && query.isMember("LabelsConstraint")) + { + Orthanc::SerializationToolbox::ReadSetOfStrings(labelsToFind, query, "Labels"); + labelsConstraint = Orthanc::SerializationToolbox::ReadString(query, "LabelsConstraint"); + } + else if (query.isMember("Labels") || query.isMember("LabelsConstraint")) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Auth plugin: unable to transform tools/find query, both 'Labels' and 'LabelsConstraint' must be defined together if one of them is defined."); + } + + if (profile.authorizedLabels.size() > 0 || profile.forbiddenLabels.size() > 0) + { + // if the user has access to all labels: no need to transform the tools/find body, we keep it as is + if (profile.authorizedLabels.find("*") == profile.authorizedLabels.end()) + { // the user does not have access to all labels -> transform the tools/find body + + if (labelsToFind.size() == 0) + { + if (profile.authorizedLabels.size() > 0) + { + Orthanc::SerializationToolbox::WriteSetOfStrings(query, profile.authorizedLabels, "Labels"); + query["LabelsConstraint"] = "Any"; + } + else if (profile.forbiddenLabels.size() > 0) + { + if (labelsToFind.size() == 0) + { // in this case, we can add a None constraint + Orthanc::SerializationToolbox::WriteSetOfStrings(query, profile.forbiddenLabels, "Labels"); + query["LabelsConstraint"] = "None"; + } + } + } + else if (labelsConstraint == "All") + { + if (profile.authorizedLabels.size() > 0) + { + if (!Orthanc::Toolbox::IsSetInSet(labelsToFind, profile.authorizedLabels)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Auth plugin: unable to transform tools/find query with 'All' labels constraint when the user does not have access to all listed labels."); + } + } + else if (profile.forbiddenLabels.size() > 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Auth plugin: unable to transform tools/find query with 'All' labels constraint when the user has forbidden labels."); + } + } + else if (labelsConstraint == "Any") + { + if (profile.authorizedLabels.size() > 0) + { + std::set newLabelsToFind; + for (std::set::const_iterator itLabel = labelsToFind.begin(); itLabel != labelsToFind.end(); ++itLabel) + { + if (profile.authorizedLabels.find(*itLabel) != profile.authorizedLabels.end()) + { + newLabelsToFind.insert(*itLabel); + } + } + + if (newLabelsToFind.size() == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Auth plugin: unable to transform tools/find query with 'All' labels constraint when none of the labels to find is authorized for the user."); + } + + query.removeMember("Labels"); + Orthanc::SerializationToolbox::WriteSetOfStrings(query, newLabelsToFind, "Labels"); + } + else if (profile.forbiddenLabels.size() > 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Auth plugin: unable to transform tools/find query with 'Any' labels constraint when the user has forbidden labels."); + } + } + else if (labelsConstraint == "None") + { + if (profile.authorizedLabels.size() > 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Auth plugin: unable to transform tools/find query with 'None' labels constraint when the user only has authorized_labels."); + } + else if (profile.forbiddenLabels.size() > 0) + { + std::set newLabelsToFind = labelsToFind; + Orthanc::Toolbox::AppendSets(newLabelsToFind, profile.forbiddenLabels); + + query.removeMember("Labels"); + Orthanc::SerializationToolbox::WriteSetOfStrings(query, newLabelsToFind, "Labels"); + } + } + } + } +} + +void ToolsFind(OrthancPluginRestOutput* output, + const char* /*url*/, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(context, output, "POST"); + } + else + { + // The filtering to this route is performed by this plugin as it is done for any other route before we get here. + + Json::Value body; + if (!OrthancPlugins::ReadJson(body, request->body, request->bodySize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected"); + } + + // If the logged in user has restrictions on the labels he can access, modify the tools/find payload before reposting it to Orthanc + OrthancPlugins::IAuthorizationService::UserProfile profile; + if (GetUserProfileInternal(profile, request)) + { + AdjustToolsFindQueryLabels(body, profile); + + Json::Value result; + if (OrthancPlugins::RestApiPost(result, "/tools/find", body, false)) + { + OrthancPlugins::AnswerJson(result, output); + } + + } + else + { + OrthancPluginSendHttpStatusCode(context, output, 403); // TODO: check + } + + + } +} + + void CreateToken(OrthancPluginRestOutput* output, const char* /*url*/, const OrthancPluginHttpRequest* request) @@ -495,6 +683,7 @@ } } + void GetUserProfile(OrthancPluginRestOutput* output, const char* /*url*/, const OrthancPluginHttpRequest* request) @@ -507,63 +696,27 @@ } else { - OrthancPlugins::AssociativeArray headers - (request->headersCount, request->headersKeys, request->headersValues, false); - - OrthancPlugins::AssociativeArray getArguments - (request->getCount, request->getKeys, request->getValues, true); - - // Loop over all the authorization tokens stored in the HTTP - // headers, until finding one that is granted - for (std::set::const_iterator - token = tokens_.begin(); token != tokens_.end(); ++token) + OrthancPlugins::IAuthorizationService::UserProfile profile; + if (GetUserProfileInternal(profile, request)) { - OrthancPlugins::IAuthorizationService::UserProfile profile; - - std::string value; - - bool hasValue = false; - switch (token->GetType()) + Json::Value jsonProfile; + jsonProfile["name"] = profile.name; + jsonProfile["permissions"] = Json::arrayValue; + for (std::set::const_iterator it = profile.permissions.begin(); it != profile.permissions.end(); ++it) { - case OrthancPlugins::TokenType_HttpHeader: - hasValue = headers.GetValue(value, token->GetKey()); - break; - - case OrthancPlugins::TokenType_GetArgument: - hasValue = getArguments.GetValue(value, token->GetKey()); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + jsonProfile["permissions"].append(*it); } - - if (hasValue) + for (std::set::const_iterator it = profile.authorizedLabels.begin(); it != profile.authorizedLabels.end(); ++it) + { + jsonProfile["authorized-labels"].append(*it); + } + for (std::set::const_iterator it = profile.forbiddenLabels.begin(); it != profile.forbiddenLabels.end(); ++it) { - unsigned int validity; // not used - if (authorizationService_->GetUserProfile(validity, profile, *token, value)) - { - Json::Value jsonProfile; - jsonProfile["name"] = profile.name; - jsonProfile["permissions"] = Json::arrayValue; - for (std::set::const_iterator it = profile.permissions.begin(); it != profile.permissions.end(); ++it) - { - jsonProfile["permissions"].append(*it); - } - for (std::set::const_iterator it = profile.authorizedLabels.begin(); it != profile.authorizedLabels.end(); ++it) - { - jsonProfile["authorized-labels"].append(*it); - } - for (std::set::const_iterator it = profile.forbiddenLabels.begin(); it != profile.forbiddenLabels.end(); ++it) - { - jsonProfile["forbidden-labels"].append(*it); - } + jsonProfile["forbidden-labels"].append(*it); + } - OrthancPlugins::AnswerJson(jsonProfile, output); - return; - } - } + OrthancPlugins::AnswerJson(jsonProfile, output); } - } } @@ -910,6 +1063,8 @@ OrthancPlugins::RegisterRestCallback("/auth/tokens/(.*)", true); } + OrthancPlugins::RegisterRestCallback("/tools/find", true); + if (authorizationParser_.get() != NULL || permissionParser_.get() != NULL) { diff -r aa56dcf599b9 -r 2b1a95c7d263 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Tue Aug 22 17:50:01 2023 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Wed Aug 30 18:10:09 2023 +0200 @@ -26,10 +26,13 @@ #include "../Plugin/DefaultAuthorizationParser.h" #include "../Plugin/AssociativeArray.h" #include "../Plugin/AccessedResource.h" +#include "../Plugin/IAuthorizationService.h" #include "../Plugin/MemoryCache.h" #include "../Plugin/PermissionParser.h" #include "../Plugin/ResourceHierarchyCache.h" +extern void AdjustToolsFindQueryLabels(Json::Value& query, const OrthancPlugins::IAuthorizationService::UserProfile& profile); + using namespace OrthancPlugins; std::string instanceOrthancId = "44444444-44444444-44444444-44444444-44444444"; @@ -298,6 +301,333 @@ ASSERT_TRUE(IsAccessing(accesses, AccessLevel_System, "/dicom-web/servers/test/qido")); } + +bool IsInJsonArray(const char* needle, const Json::Value& array) +{ + for (Json::ArrayIndex i = 0; i < array.size(); ++i) + { + if (array[i].asString() == needle) + { + return true; + } + } + return false; +} + +TEST(ToolsFindLabels, AdjustQueryForUserWithoutRestrictions) +{ + // user who has access to all labels + OrthancPlugins::IAuthorizationService::UserProfile profile; + profile.authorizedLabels.insert("*"); + + { // no labels before transformation -> no labels after + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_FALSE(query.isMember("Labels")); + ASSERT_FALSE(query.isMember("LabelsConstraint")); + } + + { // missing LabelsConstraint -> throw + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } + + { // simple 'All' label constraint is not modified since user has access to all labels + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + query["Labels"].append("b"); + query["LabelsConstraint"] = "All"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("a", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_EQ("All", query["LabelsConstraint"].asString()); + } + + { // simple 'Any' label constraint is not modified since user has access to all labels + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + query["Labels"].append("b"); + query["LabelsConstraint"] = "Any"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("a", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_EQ("Any", query["LabelsConstraint"].asString()); + } + + { // simple 'None' label constraint is not modified since user has access to all labels + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + query["Labels"].append("b"); + query["LabelsConstraint"] = "None"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("a", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_EQ("None", query["LabelsConstraint"].asString()); + } + +} + + +TEST(ToolsFindLabels, AdjustQueryForUserWithAuthorizedLabelsRestrictions) +{ + // user who has access only to "b" and "c" + OrthancPlugins::IAuthorizationService::UserProfile profile; + profile.authorizedLabels.insert("b"); + profile.authorizedLabels.insert("c"); + + { // no labels before transformation -> "b", "c" label after + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("c", query["Labels"])); + ASSERT_EQ("Any", query["LabelsConstraint"].asString()); + } + + { // missing LabelsConstraint -> throw + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } + + { // 'All' label constraint is not modified if it contains the labels that are accessible to the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["Labels"].append("c"); + query["LabelsConstraint"] = "All"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("c", query["Labels"])); + ASSERT_EQ("All", query["LabelsConstraint"].asString()); + } + + { // 'All' label constraint is not modified if it contains a subset of the labels that are accessible to the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["LabelsConstraint"] = "All"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(1u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_EQ("All", query["LabelsConstraint"].asString()); + } + + { // 'All' label constraint becomes invalid if it contains a label that is not accessible to the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + query["Labels"].append("b"); + query["LabelsConstraint"] = "All"; + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } + + { // 'Any' label constraint is not modified if it contains the labels that are accessible to the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["Labels"].append("c"); + query["LabelsConstraint"] = "Any"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("c", query["Labels"])); + ASSERT_EQ("Any", query["LabelsConstraint"].asString()); + } + + { // 'Any' label constraint is not modified if it contains a subset of the labels that are accessible to the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["LabelsConstraint"] = "Any"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(1u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_EQ("Any", query["LabelsConstraint"].asString()); + } + + { // 'Any' label constraint only contains the intersection of the initial requested labels and the ones authorized to the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + query["Labels"].append("b"); + query["LabelsConstraint"] = "Any"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(1u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_EQ("Any", query["LabelsConstraint"].asString()); + } + + { // 'Any' label constraint can not be modified if the initial requested labels have nothing in common with the authorized labels + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("d"); + query["Labels"].append("e"); + query["LabelsConstraint"] = "Any"; + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } + + { // 'None' label constraint can not be modified since the user has only 'authorized_labels' -> throw + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["Labels"].append("c"); + query["LabelsConstraint"] = "None"; + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } +} + +TEST(ToolsFindLabels, AdjustQueryForUserWithForbiddenLabelsRestrictions) +{ + // user who has forbidden access to "b" and "c" + OrthancPlugins::IAuthorizationService::UserProfile profile; + profile.forbiddenLabels.insert("b"); + profile.forbiddenLabels.insert("c"); + + { // no labels before transformation -> "b", "c" label after (with a 'None' constraint) + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + + AdjustToolsFindQueryLabels(query, profile); + + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("c", query["Labels"])); + ASSERT_EQ("None", query["LabelsConstraint"].asString()); + } + + { // missing LabelsConstraint -> throw + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("a"); + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } + + { // 'All' label constraint can not be modified for user with forbidden labels + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["Labels"].append("c"); + query["LabelsConstraint"] = "All"; + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } + + { // 'Any' label constraint can not be modified for user with forbidden labels + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["Labels"].append("c"); + query["LabelsConstraint"] = "Any"; + + ASSERT_THROW(AdjustToolsFindQueryLabels(query, profile), Orthanc::OrthancException); + } + + { // 'None' label constraint are modified to always contain at least all forbidden_labels of the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("b"); + query["LabelsConstraint"] = "None"; + + AdjustToolsFindQueryLabels(query, profile); + ASSERT_EQ(2u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("c", query["Labels"])); + ASSERT_EQ("None", query["LabelsConstraint"].asString()); + } + + { // 'None' label constraint are modified to always contain at least all forbidden_labels of the user + Json::Value query; + query["Query"] = Json::objectValue; + query["Query"]["PatientID"] = "*"; + query["Labels"] = Json::arrayValue; + query["Labels"].append("d"); + query["LabelsConstraint"] = "None"; + + AdjustToolsFindQueryLabels(query, profile); + ASSERT_EQ(3u, query["Labels"].size()); + ASSERT_TRUE(IsInJsonArray("b", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("c", query["Labels"])); + ASSERT_TRUE(IsInJsonArray("d", query["Labels"])); + ASSERT_EQ("None", query["LabelsConstraint"].asString()); + } +} + } int main(int argc, char **argv)