#include "access.hpp" #include "common/time_utils.hpp" #include "common/uuid.hpp" #include "logging/logger.hpp" namespace smartbotic::webserver::auth { using namespace common; namespace { constexpr const char* PROJECTS = "projects"; } InstanceRole instanceRoleFromString(const std::string& role) { if (role == "owner") return InstanceRole::Owner; if (role == "admin") return InstanceRole::Admin; // "user" is what the role was called before there were three of them, and // stored accounts still say it. return InstanceRole::Member; } std::string instanceRoleToString(InstanceRole role) { switch (role) { case InstanceRole::Owner: return "owner"; case InstanceRole::Admin: return "admin"; case InstanceRole::Member: return "member"; } return "member"; } ProjectRole projectRoleFromString(const std::string& role) { if (role == "admin") return ProjectRole::Admin; if (role == "editor") return ProjectRole::Editor; if (role == "viewer") return ProjectRole::Viewer; return ProjectRole::None; } std::string projectRoleToString(ProjectRole role) { switch (role) { case ProjectRole::Admin: return "admin"; case ProjectRole::Editor: return "editor"; case ProjectRole::Viewer: return "viewer"; case ProjectRole::None: return "none"; } return "none"; } AccessControl::AccessControl(storage::StorageClient& storage) : storage_(storage) {} std::string AccessControl::projectOf(const nlohmann::json& record) { return record.value("projectId", std::string()); } std::unordered_set AccessControl::projectsFor(const AuthContext& ctx) { std::unordered_set out; storage::QueryOptions options; options.page_size = 1000; auto result = storage_.query(PROJECTS, options); if (result.failed()) { LOG_WARN("Could not read projects for access check: {}", result.error().message()); return out; } const bool sees_everything = instanceRoleFromString(ctx.role) != InstanceRole::Member; for (const auto& project : result.value().documents) { const std::string id = project.value("_id", ""); if (id.empty()) continue; if (sees_everything) { out.insert(id); continue; } if (project.value("ownerId", "") == ctx.user_id) { out.insert(id); continue; } for (const auto& member : project.value("members", nlohmann::json::array())) { if (member.value("userId", "") == ctx.user_id) { out.insert(id); break; } } } return out; } ProjectRole AccessControl::roleIn(const AuthContext& ctx, const std::string& project_id) { if (instanceRoleFromString(ctx.role) != InstanceRole::Member) { return ProjectRole::Admin; } if (project_id.empty()) return ProjectRole::None; auto project = storage_.get(PROJECTS, project_id); if (project.failed()) return ProjectRole::None; // A personal project belongs entirely to its owner - there is no // membership row to look up and none can be added. if (project.value().value("ownerId", "") == ctx.user_id) return ProjectRole::Admin; for (const auto& member : project.value().value("members", nlohmann::json::array())) { if (member.value("userId", "") == ctx.user_id) { return projectRoleFromString(member.value("role", "viewer")); } } return ProjectRole::None; } bool AccessControl::allowed(const AuthContext& ctx, const std::string& project_id, Action action) { const ProjectRole role = roleIn(ctx, project_id); switch (action) { case Action::Read: return role != ProjectRole::None; case Action::Run: // A viewer may watch but not start. Running a workflow sends email, // writes to collections and spends money at an API - it is not a // read however little it changes the workflow itself. return role == ProjectRole::Editor || role == ProjectRole::Admin; case Action::Write: return role == ProjectRole::Editor || role == ProjectRole::Admin; case Action::Manage: return role == ProjectRole::Admin; } return false; } common::Result AccessControl::personalProjectFor(const std::string& user_id) { storage::QueryOptions options; options.page_size = 1000; options.filters.push_back({"type", "personal"}); auto result = storage_.query(PROJECTS, options); if (result.failed()) return Error(ErrorCode::DatabaseError, result.error().message()); for (const auto& project : result.value().documents) { if (project.value("ownerId", "") == user_id) { return project.value("_id", std::string()); } } return Error(ErrorCode::NotFound, "No personal project for user " + user_id); } common::Result AccessControl::ensurePersonalProject(const std::string& user_id, const std::string& username) { auto existing = personalProjectFor(user_id); if (existing.ok()) { auto project = storage_.get(PROJECTS, existing.value()); if (project.ok()) return project.value(); } const std::string id = "prj_" + UUID::generate(); nlohmann::json project = { {"name", username.empty() ? std::string("Personal") : username + "'s project"}, {"type", "personal"}, {"ownerId", user_id}, {"members", nlohmann::json::array()}, {"createdAt", TimeUtils::nowMs()}, }; auto inserted = storage_.insert(PROJECTS, project, id); if (inserted.failed()) { return Error(ErrorCode::DatabaseError, inserted.error().message()); } project["_id"] = id; LOG_INFO("Created personal project {} for user {}", id, user_id); return project; } } // namespace smartbotic::webserver::auth