access.cpp 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #include "access.hpp"
  2. #include "common/time_utils.hpp"
  3. #include "common/uuid.hpp"
  4. #include "logging/logger.hpp"
  5. namespace smartbotic::webserver::auth {
  6. using namespace common;
  7. namespace {
  8. constexpr const char* PROJECTS = "projects";
  9. }
  10. InstanceRole instanceRoleFromString(const std::string& role) {
  11. if (role == "owner") return InstanceRole::Owner;
  12. if (role == "admin") return InstanceRole::Admin;
  13. // "user" is what the role was called before there were three of them, and
  14. // stored accounts still say it.
  15. return InstanceRole::Member;
  16. }
  17. std::string instanceRoleToString(InstanceRole role) {
  18. switch (role) {
  19. case InstanceRole::Owner: return "owner";
  20. case InstanceRole::Admin: return "admin";
  21. case InstanceRole::Member: return "member";
  22. }
  23. return "member";
  24. }
  25. ProjectRole projectRoleFromString(const std::string& role) {
  26. if (role == "admin") return ProjectRole::Admin;
  27. if (role == "editor") return ProjectRole::Editor;
  28. if (role == "viewer") return ProjectRole::Viewer;
  29. return ProjectRole::None;
  30. }
  31. std::string projectRoleToString(ProjectRole role) {
  32. switch (role) {
  33. case ProjectRole::Admin: return "admin";
  34. case ProjectRole::Editor: return "editor";
  35. case ProjectRole::Viewer: return "viewer";
  36. case ProjectRole::None: return "none";
  37. }
  38. return "none";
  39. }
  40. AccessControl::AccessControl(storage::StorageClient& storage) : storage_(storage) {}
  41. std::string AccessControl::projectOf(const nlohmann::json& record) {
  42. return record.value("projectId", std::string());
  43. }
  44. std::unordered_set<std::string> AccessControl::projectsFor(const AuthContext& ctx) {
  45. std::unordered_set<std::string> out;
  46. storage::QueryOptions options;
  47. options.page_size = 1000;
  48. auto result = storage_.query(PROJECTS, options);
  49. if (result.failed()) {
  50. LOG_WARN("Could not read projects for access check: {}", result.error().message());
  51. return out;
  52. }
  53. const bool sees_everything = instanceRoleFromString(ctx.role) != InstanceRole::Member;
  54. for (const auto& project : result.value().documents) {
  55. const std::string id = project.value("_id", "");
  56. if (id.empty()) continue;
  57. if (sees_everything) {
  58. out.insert(id);
  59. continue;
  60. }
  61. if (project.value("ownerId", "") == ctx.user_id) {
  62. out.insert(id);
  63. continue;
  64. }
  65. for (const auto& member : project.value("members", nlohmann::json::array())) {
  66. if (member.value("userId", "") == ctx.user_id) {
  67. out.insert(id);
  68. break;
  69. }
  70. }
  71. }
  72. return out;
  73. }
  74. ProjectRole AccessControl::roleIn(const AuthContext& ctx, const std::string& project_id) {
  75. if (instanceRoleFromString(ctx.role) != InstanceRole::Member) {
  76. return ProjectRole::Admin;
  77. }
  78. if (project_id.empty()) return ProjectRole::None;
  79. auto project = storage_.get(PROJECTS, project_id);
  80. if (project.failed()) return ProjectRole::None;
  81. // A personal project belongs entirely to its owner - there is no
  82. // membership row to look up and none can be added.
  83. if (project.value().value("ownerId", "") == ctx.user_id) return ProjectRole::Admin;
  84. for (const auto& member : project.value().value("members", nlohmann::json::array())) {
  85. if (member.value("userId", "") == ctx.user_id) {
  86. return projectRoleFromString(member.value("role", "viewer"));
  87. }
  88. }
  89. return ProjectRole::None;
  90. }
  91. bool AccessControl::allowed(const AuthContext& ctx, const std::string& project_id, Action action) {
  92. const ProjectRole role = roleIn(ctx, project_id);
  93. switch (action) {
  94. case Action::Read:
  95. return role != ProjectRole::None;
  96. case Action::Run:
  97. // A viewer may watch but not start. Running a workflow sends email,
  98. // writes to collections and spends money at an API - it is not a
  99. // read however little it changes the workflow itself.
  100. return role == ProjectRole::Editor || role == ProjectRole::Admin;
  101. case Action::Write:
  102. return role == ProjectRole::Editor || role == ProjectRole::Admin;
  103. case Action::Manage:
  104. return role == ProjectRole::Admin;
  105. }
  106. return false;
  107. }
  108. common::Result<std::string> AccessControl::personalProjectFor(const std::string& user_id) {
  109. storage::QueryOptions options;
  110. options.page_size = 1000;
  111. options.filters.push_back({"type", "personal"});
  112. auto result = storage_.query(PROJECTS, options);
  113. if (result.failed()) return Error(ErrorCode::DatabaseError, result.error().message());
  114. for (const auto& project : result.value().documents) {
  115. if (project.value("ownerId", "") == user_id) {
  116. return project.value("_id", std::string());
  117. }
  118. }
  119. return Error(ErrorCode::NotFound, "No personal project for user " + user_id);
  120. }
  121. common::Result<nlohmann::json> AccessControl::ensurePersonalProject(const std::string& user_id,
  122. const std::string& username) {
  123. auto existing = personalProjectFor(user_id);
  124. if (existing.ok()) {
  125. auto project = storage_.get(PROJECTS, existing.value());
  126. if (project.ok()) return project.value();
  127. }
  128. const std::string id = "prj_" + UUID::generate();
  129. nlohmann::json project = {
  130. {"name", username.empty() ? std::string("Personal") : username + "'s project"},
  131. {"type", "personal"},
  132. {"ownerId", user_id},
  133. {"members", nlohmann::json::array()},
  134. {"createdAt", TimeUtils::nowMs()},
  135. };
  136. auto inserted = storage_.insert(PROJECTS, project, id);
  137. if (inserted.failed()) {
  138. return Error(ErrorCode::DatabaseError, inserted.error().message());
  139. }
  140. project["_id"] = id;
  141. LOG_INFO("Created personal project {} for user {}", id, user_id);
  142. return project;
  143. }
  144. } // namespace smartbotic::webserver::auth