|
|
@@ -7,9 +7,11 @@ namespace smartbotic::webserver::api {
|
|
|
using namespace common;
|
|
|
|
|
|
WorkflowGroupController::WorkflowGroupController(storage::StorageClient& storage,
|
|
|
+ auth::AccessControl& access,
|
|
|
auth::AuthMiddleware& middleware,
|
|
|
WebSocketServer& ws_server)
|
|
|
: storage_(storage)
|
|
|
+ , access_(access)
|
|
|
, middleware_(middleware)
|
|
|
, ws_server_(ws_server) {}
|
|
|
|
|
|
@@ -64,6 +66,30 @@ void WorkflowGroupController::registerRoutes(httplib::Server& server) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+bool WorkflowGroupController::loadAllowed(httplib::Response& res, const auth::AuthContext& ctx,
|
|
|
+ const std::string& id, auth::Action action,
|
|
|
+ nlohmann::json& out) {
|
|
|
+ auto stored = storage_.get("workflow_groups", id);
|
|
|
+ if (stored.failed()) {
|
|
|
+ sendError(res, "Folder not found", 404);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const std::string project = auth::AccessControl::projectOf(stored.value());
|
|
|
+ if (!access_.allowed(ctx, project, auth::Action::Read)) {
|
|
|
+ // As with a workflow: whether it exists is only told to people who can
|
|
|
+ // reach it.
|
|
|
+ sendError(res, "Folder not found", 404);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (action != auth::Action::Read && !access_.allowed(ctx, project, action)) {
|
|
|
+ sendError(res, "You can see this folder but not change it", 403);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ out = stored.value();
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
void WorkflowGroupController::listGroups(const httplib::Request& req, httplib::Response& res,
|
|
|
const auth::AuthContext& ctx) {
|
|
|
storage::QueryOptions options;
|
|
|
@@ -99,9 +125,17 @@ void WorkflowGroupController::listGroups(const httplib::Request& req, httplib::R
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // Only the projects this caller can reach. A folder names somebody's work
|
|
|
+ // and shows how it is arranged, which is enough to be worth withholding.
|
|
|
+ const auto reachable = access_.projectsFor(ctx);
|
|
|
+ nlohmann::json visible = nlohmann::json::array();
|
|
|
+ for (const auto& group : result.value().documents) {
|
|
|
+ if (reachable.contains(auth::AccessControl::projectOf(group))) visible.push_back(group);
|
|
|
+ }
|
|
|
+
|
|
|
nlohmann::json response;
|
|
|
- response["groups"] = result.value().documents;
|
|
|
- response["total"] = result.value().total_count;
|
|
|
+ response["groups"] = visible;
|
|
|
+ response["total"] = visible.size();
|
|
|
response["page"] = page;
|
|
|
response["pageSize"] = page_size;
|
|
|
response["hasMore"] = result.value().has_more;
|
|
|
@@ -113,13 +147,10 @@ void WorkflowGroupController::getGroup(const httplib::Request& req, httplib::Res
|
|
|
const auth::AuthContext& ctx) {
|
|
|
std::string id = req.matches[1];
|
|
|
|
|
|
- auto result = storage_.get("workflow_groups", id);
|
|
|
- if (result.failed()) {
|
|
|
- sendError(res, "Group not found", 404);
|
|
|
- return;
|
|
|
- }
|
|
|
+ nlohmann::json group;
|
|
|
+ if (!loadAllowed(res, ctx, id, auth::Action::Read, group)) return;
|
|
|
|
|
|
- sendJson(res, result.value());
|
|
|
+ sendJson(res, group);
|
|
|
}
|
|
|
|
|
|
void WorkflowGroupController::createGroup(const httplib::Request& req, httplib::Response& res,
|
|
|
@@ -133,6 +164,29 @@ void WorkflowGroupController::createGroup(const httplib::Request& req, httplib::
|
|
|
// Set owner
|
|
|
body["ownerId"] = ctx.user_id;
|
|
|
|
|
|
+ // A folder belongs to a project like the workflows in it. Without one
|
|
|
+ // it would be reachable only by an admin, which is not what somebody
|
|
|
+ // making a folder meant.
|
|
|
+ std::string project = body.value("projectId", "");
|
|
|
+ if (!project.empty() && !access_.allowed(ctx, project, auth::Action::Write)) {
|
|
|
+ sendError(res, "You cannot create a folder in that project", 403);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (project.empty()) {
|
|
|
+ auto personal = access_.personalProjectFor(ctx.user_id);
|
|
|
+ if (personal.ok()) {
|
|
|
+ project = personal.value();
|
|
|
+ } else {
|
|
|
+ auto created = access_.ensurePersonalProject(ctx.user_id, ctx.username);
|
|
|
+ if (created.failed()) {
|
|
|
+ sendError(res, "You have no project to put this in", 500);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ project = created.value().value("_id", "");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ body["projectId"] = project;
|
|
|
+
|
|
|
// Validate required fields
|
|
|
if (!body.contains("name") || body["name"].get<std::string>().empty()) {
|
|
|
sendError(res, "Name is required", 400);
|
|
|
@@ -182,6 +236,10 @@ void WorkflowGroupController::updateGroup(const httplib::Request& req, httplib::
|
|
|
const auth::AuthContext& ctx) {
|
|
|
try {
|
|
|
std::string id = req.matches[1];
|
|
|
+
|
|
|
+ nlohmann::json existing;
|
|
|
+ if (!loadAllowed(res, ctx, id, auth::Action::Write, existing)) return;
|
|
|
+
|
|
|
auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
|
// Prevent updating metadata fields
|
|
|
@@ -219,6 +277,9 @@ void WorkflowGroupController::deleteGroup(const httplib::Request& req, httplib::
|
|
|
const auth::AuthContext& ctx) {
|
|
|
std::string id = req.matches[1];
|
|
|
|
|
|
+ nlohmann::json existing;
|
|
|
+ if (!loadAllowed(res, ctx, id, auth::Action::Write, existing)) return;
|
|
|
+
|
|
|
// Check force parameter
|
|
|
bool force = req.has_param("force") && req.get_param_value("force") == "true";
|
|
|
|
|
|
@@ -284,12 +345,9 @@ void WorkflowGroupController::getGroupPath(const httplib::Request& req, httplib:
|
|
|
const auth::AuthContext& ctx) {
|
|
|
std::string id = req.matches[1];
|
|
|
|
|
|
- // Verify group exists
|
|
|
- auto result = storage_.get("workflow_groups", id);
|
|
|
- if (result.failed()) {
|
|
|
- sendError(res, "Group not found", 404);
|
|
|
- return;
|
|
|
- }
|
|
|
+ nlohmann::json group;
|
|
|
+ if (!loadAllowed(res, ctx, id, auth::Action::Read, group)) return;
|
|
|
+ auto result = common::Result<nlohmann::json>(group);
|
|
|
|
|
|
// Build path from root to this group
|
|
|
auto path = buildGroupPath(id);
|
|
|
@@ -303,6 +361,10 @@ void WorkflowGroupController::moveGroup(const httplib::Request& req, httplib::Re
|
|
|
const auth::AuthContext& ctx) {
|
|
|
try {
|
|
|
std::string id = req.matches[1];
|
|
|
+
|
|
|
+ nlohmann::json existing;
|
|
|
+ if (!loadAllowed(res, ctx, id, auth::Action::Write, existing)) return;
|
|
|
+
|
|
|
auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
|
// Get new parent ID - can be null/empty/"root" for top-level
|