Skip to content

Commit

Permalink
Add algorithm to visit Server/Defs/Nodes on a path
Browse files Browse the repository at this point in the history
This algorithm allows visiting the nodes on a given path and to collect
the applicable per node permission used for Authorisation

Re ECFLOW-1960
  • Loading branch information
marcosbento committed Feb 26, 2025
1 parent ae72f8c commit 5e5ff72
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 38 deletions.
1 change: 1 addition & 0 deletions libs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ set(srcs
# Base -- Headers
base/src/ecflow/base/AbstractClientEnv.hpp
base/src/ecflow/base/AbstractServer.hpp
base/src/ecflow/base/Algorithms.hpp
base/src/ecflow/base/Authentication.hpp
base/src/ecflow/base/AuthenticationDetails.hpp
base/src/ecflow/base/Authorisation.hpp
Expand Down
24 changes: 24 additions & 0 deletions libs/base/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ target_clangformat(u_base
CONDITION ENABLE_TESTS
)

set(test_srcs
# Sources
test/TestAlgorithms.cpp
)

ecbuild_add_test(
TARGET
u_base_algorithms
LABELS
unit nightly
SOURCES
${test_srcs}
LIBS
ecflow_all
test_scaffold
test_harness.base
Threads::Threads
$<$<BOOL:${OPENSSL_FOUND}>:OpenSSL::SSL>
)
target_clangformat(u_base_algorithms
CONDITION ENABLE_TESTS
)


# The following is not technically a test (as it makes no checks),
# but a tool to measure the time it takes to generate a job file
if (ENABLE_ALL_TESTS)
Expand Down
117 changes: 117 additions & 0 deletions libs/base/src/ecflow/base/Algorithms.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2009- ECMWF.
*
* This software is licensed under the terms of the Apache Licence version 2.0
* which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
* In applying this licence, ECMWF does not waive the privileges and immunities
* granted to it by virtue of its status as an intergovernmental organisation
* nor does it submit to any jurisdiction.
*/

#ifndef ecflow_base_Algorithms_hpp
#define ecflow_base_Algorithms_hpp

#include <string>

#include <boost/algorithm/string.hpp>
#include <boost/tokenizer.hpp>

#include "ecflow/base/AbstractServer.hpp"
#include "ecflow/core/Result.hpp"
#include "ecflow/node/Defs.hpp"

namespace ecf {

///
/// Represents a path in the server's hierarchy
///
/// A path object is always represents a valid path (even if it does not exist).
///
/// A path with 0 tokens represents the root of the hierarchy (represented by a slash, "/").
/// A path with N tokens represents a path from the root to a node.
///
/// Multiple consecutive separators (i.e. slashes, "/") are treated as a single slash.
///
struct Path
{
static Result<Path> make(const std::string& path) {
if (path.empty()) {
return Result<Path>::failure("Invalid path: '" + path + "' (cannot be empty)");
}

if (path == "/") {
return Result<Path>::success(Path(std::vector<std::string>()));
}

std::vector<std::string> tokens;
boost::tokenizer<boost::char_separator<char>> tokenizer(path, boost::char_separator<char>("/ "));
for (const auto& token : tokenizer) {
tokens.push_back(token);
}
return Result<Path>::success(Path(std::move(tokens)));
}

[[nodiscard]] std::string to_string() const {
if (tokens_.empty()) {
return "/";
}

std::string result;
for (auto&& token : tokens_) {
result += '/';
result += token;
}
return result;
}

[[nodiscard]] bool empty() const { return tokens_.empty(); }
[[nodiscard]] size_t size() const { return tokens_.size(); }
[[nodiscard]] const std::string& operator[](size_t idx) const { return tokens_[idx]; }

auto begin() const { return tokens_.begin(); }
auto end() const { return tokens_.end(); }

private:
explicit Path(std::vector<std::string> tokens) : tokens_(std::move(tokens)) {}

std::vector<std::string> tokens_;
};

template <typename PREDICATE>
void visit(const Defs& defs, const Path& path, PREDICATE& predicate) {

// a. Visit the server 'definitions'
predicate(defs);

// b. Visit each one of the 'nodes' along the given path

node_ptr current = nullptr;
for (auto&& token : path) {
if (current == nullptr) {
current = defs.findSuite(token);
}
else {
current = current->find_immediate_child(token);
}
predicate(*current);
}

}

template <typename PREDICATE>
void visit(const AbstractServer& server, const Path& path, PREDICATE& predicate) {

// Visit the 'server'
predicate(server);

if (path.empty()) {
return;
}

visit(*server.defs(), path, predicate);
}


} // namespace ecf

#endif // ecflow_base_Algorithms_hpp
146 changes: 146 additions & 0 deletions libs/base/test/TestAlgorithms.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2009- ECMWF.
*
* This software is licensed under the terms of the Apache Licence version 2.0
* which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
* In applying this licence, ECMWF does not waive the privileges and immunities
* granted to it by virtue of its status as an intergovernmental organisation
* nor does it submit to any jurisdiction.
*/

#define BOOST_TEST_MODULE Test_Base
#include <boost/test/included/unit_test.hpp>

#include "MockServer.hpp"
#include "ecflow/base/Algorithms.hpp"
#include "ecflow/node/Family.hpp"
#include "ecflow/server/BaseServer.hpp"
#include "ecflow/test/scaffold/Naming.hpp"

BOOST_AUTO_TEST_SUITE(U_Base)

BOOST_AUTO_TEST_SUITE(T_Path)

BOOST_AUTO_TEST_CASE(test_cannot_create_path_from_empty_string) {
ECF_NAME_THIS_TEST();

auto result = ecf::Path::make("");
BOOST_CHECK_MESSAGE(!result.ok(), "expected !ok");
}

BOOST_AUTO_TEST_CASE(test_can_create_path_from_root_only) {
ECF_NAME_THIS_TEST();

auto result = ecf::Path::make("/");
BOOST_CHECK(result.ok());
auto path = result.value();
BOOST_CHECK(path.empty());
BOOST_CHECK_EQUAL(path.size(), 0ul);
BOOST_CHECK_EQUAL(path.to_string(), "/");
}

BOOST_AUTO_TEST_CASE(test_can_create_path_with_single_token) {
ECF_NAME_THIS_TEST();

auto result = ecf::Path::make("/suite");
BOOST_CHECK(result.ok());
auto path = result.value();
BOOST_CHECK(!path.empty());
BOOST_CHECK_EQUAL(path.size(), 1ul);
BOOST_CHECK_EQUAL(path[0], "suite");
BOOST_CHECK_EQUAL(path.to_string(), "/suite");
}

BOOST_AUTO_TEST_CASE(test_can_create_path_with_multiple_tokens) {
ECF_NAME_THIS_TEST();

auto result = ecf::Path::make("/suite/family/task");
BOOST_CHECK(result.ok());
auto path = result.value();
BOOST_CHECK(!path.empty());
BOOST_CHECK_EQUAL(path.size(), 3ul);
BOOST_CHECK_EQUAL(path[0], "suite");
BOOST_CHECK_EQUAL(path[1], "family");
BOOST_CHECK_EQUAL(path[2], "task");
BOOST_CHECK_EQUAL(path.to_string(), "/suite/family/task");
}

BOOST_AUTO_TEST_CASE(test_can_create_path_with_empty_tokens) {
ECF_NAME_THIS_TEST();

auto result = ecf::Path::make("///suite///family///task");
BOOST_CHECK(result.ok());
auto path = result.value();
BOOST_CHECK(!path.empty());
BOOST_CHECK_EQUAL(path.size(), 3ul);
BOOST_CHECK_EQUAL(path[0], "suite");
BOOST_CHECK_EQUAL(path[1], "family");
BOOST_CHECK_EQUAL(path[2], "task");
BOOST_CHECK_EQUAL(path.to_string(), "/suite/family/task");
}

BOOST_AUTO_TEST_SUITE_END() // T_Path

BOOST_AUTO_TEST_SUITE(T_Algorithms)

BOOST_AUTO_TEST_CASE(test_can_visit_defs) {
ECF_NAME_THIS_TEST();

Defs defs;
suite_ptr s = defs.add_suite("suite");
family_ptr f = s->add_family("family");
task_ptr t = f->add_task("task");

struct Visitor
{
void operator()(const Defs& defs) { collected.push_back("defs"); }
void operator()(const Node& s) { collected.push_back("node: " + s.name()); }

std::vector<std::string> collected;
};

auto path = ecf::Path::make("/suite/family/task").value();
Visitor visitor;
ecf::visit(defs, path, visitor);

BOOST_CHECK_EQUAL(visitor.collected.size(), 4ul);
BOOST_CHECK_EQUAL(visitor.collected[0], "defs");
BOOST_CHECK_EQUAL(visitor.collected[1], "node: suite");
BOOST_CHECK_EQUAL(visitor.collected[2], "node: family");
BOOST_CHECK_EQUAL(visitor.collected[3], "node: task");
}

BOOST_AUTO_TEST_CASE(test_can_visit_server) {
ECF_NAME_THIS_TEST();

Defs defs;
suite_ptr s = defs.add_suite("suite");
family_ptr f = s->add_family("family");
task_ptr t = f->add_task("task");

MockServer server(&defs);

struct Visitor
{
void operator()(const AbstractServer& server) { collected.push_back("server"); }
void operator()(const Defs& defs) { collected.push_back("defs"); }
void operator()(const Node& s) { collected.push_back("node: " + s.name()); }

std::vector<std::string> collected;
};

auto path = ecf::Path::make("/suite/family/task").value();
Visitor visitor;
ecf::visit(server, path, visitor);

BOOST_CHECK_EQUAL(visitor.collected.size(), 5ul);
BOOST_CHECK_EQUAL(visitor.collected[0], "server");
BOOST_CHECK_EQUAL(visitor.collected[1], "defs");
BOOST_CHECK_EQUAL(visitor.collected[2], "node: suite");
BOOST_CHECK_EQUAL(visitor.collected[3], "node: family");
BOOST_CHECK_EQUAL(visitor.collected[4], "node: task");
}

BOOST_AUTO_TEST_SUITE_END() // T_Algorithms

BOOST_AUTO_TEST_SUITE_END()
38 changes: 0 additions & 38 deletions libs/node/src/ecflow/node/Defs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,6 @@ bool Defs::operator==(const Defs& rhs) const {
}

node_ptr Defs::findAbsNode(const std::string& pathToNode) const {
// std::cout << "Defs::findAbsNode " << pathToNode << "\n";
// The pathToNode is of the form:
// /suite
// /suite/family
Expand All @@ -1031,70 +1030,33 @@ node_ptr Defs::findAbsNode(const std::string& pathToNode) const {
StringSplitter string_splitter(pathToNode, Str::PATH_SEPARATOR());
while (!string_splitter.finished()) {
std::string_view path_token = string_splitter.next();
// std::cout << "path_token:'" << path_token << "' last = " << string_splitter.last() << "\n";
if (!first) {
for (const auto& suite : suiteVec_) {
if (path_token == suite->name()) {
ret = suite;
if (string_splitter.last()) {
// cout << "finished returning " << ret->absNodePath() << " *last* suite found\n";
return ret;
}
break;
}
}
if (!ret) {
// cout << "finished returning suite not found\n";
return node_ptr();
}
first = true;
}
else {
// cout << "seraching from " << ret->absNodePath() << " for " << ref << "\n";
ret = ret->find_immediate_child(path_token);
if (ret) {
if (string_splitter.last()) {
// cout << "finished returning " << ret->absNodePath() << " *last* \n";
return ret;
}
continue;
}
// cout << "finished returning node not found\n";
return node_ptr();
}
}
// cout << "finished returning node not found, end of loop\n";
return node_ptr();

// OLD code, slower. Since we are creating a vector of strings
// std::vector<std::string> theNodeNames; theNodeNames.reserve(4);
// NodePath::split(pathToNode,theNodeNames);
// if ( theNodeNames.empty() ) {
// return node_ptr();
// }
// size_t child_pos = 0 ; // unused
// size_t pathSize = theNodeNames.size();
// size_t theSuiteVecSize = suiteVec_.size();
// for(size_t s = 0; s < theSuiteVecSize; s++) {
// size_t index = 0;
// if (theNodeNames[index] == suiteVec_[s]->name()) {
// node_ptr the_node = suiteVec_[s];
// if (pathSize == 1) return the_node;
// index++; // skip over suite,
// while (index < pathSize) {
// the_node = the_node->findImmediateChild(theNodeNames[index],child_pos);
// if (the_node) {
// if (index == pathSize - 1) return the_node;
// index++;
// }
// else {
// return node_ptr();
// }
// }
// return node_ptr();
// }
// }
// return node_ptr();
}

node_ptr Defs::find_closest_matching_node(const std::string& pathToNode) const {
Expand Down
Loading

0 comments on commit 5e5ff72

Please sign in to comment.