diff options
| author | Dan Engelbrecht <[email protected]> | 2024-10-23 10:31:43 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-10-23 10:31:43 +0200 |
| commit | 530ab3394938331f224058c381a1db5d4a68e6a9 (patch) | |
| tree | 5060eb394d67b7454855aed0fa8d7d3acf5f5c98 /src/zenserver/workspaces/httpworkspaces.cpp | |
| parent | fix gc date (#204) (diff) | |
| download | zen-530ab3394938331f224058c381a1db5d4a68e6a9.tar.xz zen-530ab3394938331f224058c381a1db5d4a68e6a9.zip | |
workspace share security (#192)
- Improvement: Reworked workspace shares to be more secure. Workspaces and workspace shares can only be created using the `zen workspace` command, the http endpoint is disabled unless zenserver is started with the `--workspaces-allow-changes` option enabled.
- Each workspace are now configured via a `zenworkspaceconfig.json` file in the root of each workspace
- A workspace can allow shares to be created via the http interface if the workspace is created with the `--allow-share-create-from-http` option enabled
- A new http endpoint at `/ws` - issuing a `Get` operation will get you a list of workspaces
- A new http endpoint at `/ws/refresh` - issuing a `Get` will make zenserver scan for edits in workspaces and workspace shares
Diffstat (limited to 'src/zenserver/workspaces/httpworkspaces.cpp')
| -rw-r--r-- | src/zenserver/workspaces/httpworkspaces.cpp | 321 |
1 files changed, 236 insertions, 85 deletions
diff --git a/src/zenserver/workspaces/httpworkspaces.cpp b/src/zenserver/workspaces/httpworkspaces.cpp index 6a4e9c466..6e27b0f1e 100644 --- a/src/zenserver/workspaces/httpworkspaces.cpp +++ b/src/zenserver/workspaces/httpworkspaces.cpp @@ -29,19 +29,51 @@ namespace { return {}; } - Oid PathToChunkId(const std::filesystem::path& Path) + void WriteWorkspaceConfig(CbWriter& Writer, const Workspaces::WorkspaceConfiguration& Config) { - const std::string PathBuffer = reinterpret_cast<const char*>(Path.generic_u8string().c_str()); - BLAKE3 Hash = BLAKE3::HashMemory(PathBuffer.data(), PathBuffer.size()); - Hash.Hash[11] = 7; // FIoChunkType::ExternalFile - return Oid::FromMemory(Hash.Hash); - } + Writer << "id" << Config.Id; + Writer << "root_path" << Config.RootPath.string(); // utf8? + Writer << "allow_share_creation_from_http" << Config.AllowShareCreationFromHttp; + }; + + void WriteWorkspaceShareConfig(CbWriter& Writer, const Workspaces::WorkspaceShareConfiguration& Config) + { + Writer << "id" << Config.Id; + Writer << "share_path" << Config.SharePath.string(); // utf8? + if (!Config.Alias.empty()) + { + Writer << "alias" << Config.Alias; + } + }; - constinit AsciiSet ValidAliasCharactersSet{"abcdefghijklmnopqrstuvwxyz0123456789+-_.[]ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; + void WriteWorkspaceAndSharesConfig(CbWriter& Writer, Workspaces& Workspaces, const Workspaces::WorkspaceConfiguration& WorkspaceConfig) + { + WriteWorkspaceConfig(Writer, WorkspaceConfig); + if (std::optional<std::vector<Oid>> ShareIds = Workspaces.GetWorkspaceShares(WorkspaceConfig.Id); ShareIds) + { + for (const Oid& ShareId : *ShareIds) + { + Writer.BeginArray("shares"); + { + if (std::optional<Workspaces::WorkspaceShareConfiguration> WorkspaceShareConfig = + Workspaces.GetWorkspaceShareConfiguration(WorkspaceConfig.Id, ShareId); + WorkspaceShareConfig) + { + Writer.BeginObject(); + { + WriteWorkspaceShareConfig(Writer, *WorkspaceShareConfig); + } + Writer.EndObject(); + } + } + Writer.EndArray(); + } + } + } } // namespace -HttpWorkspacesService::HttpWorkspacesService(HttpStatsService& StatsService, const FileServeConfig& Cfg, Workspaces& Workspaces) +HttpWorkspacesService::HttpWorkspacesService(HttpStatsService& StatsService, const WorkspacesServeConfig& Cfg, Workspaces& Workspaces) : m_Log(logging::Get("workspaces")) , m_StatsService(StatsService) , m_Config(Cfg) @@ -125,7 +157,7 @@ HttpWorkspacesService::Initialize() m_StatsService.RegisterHandler("ws", *this); - m_Router.AddPattern("workspace", "([[:xdigit:]]{24})"); + m_Router.AddPattern("workspace_id", "([[:xdigit:]]{24})"); m_Router.AddPattern("share_id", "([[:xdigit:]]{24})"); m_Router.AddPattern("chunk", "([[:xdigit:]]{24})"); m_Router.AddPattern("share_alias", "([[:alnum:]_.\\+\\-\\[\\]]+)"); @@ -195,7 +227,17 @@ HttpWorkspacesService::Initialize() [this](HttpRouterRequest& Req) { WorkspaceRequest(Req); }, HttpVerb::kPut | HttpVerb::kGet | HttpVerb::kDelete); - ReadState(); + m_Router.RegisterRoute( + "refresh", + [this](HttpRouterRequest& Req) { RefreshRequest(Req); }, + HttpVerb::kGet); + + m_Router.RegisterRoute( + "", + [this](HttpRouterRequest& Req) { WorkspacesRequest(Req); }, + HttpVerb::kGet); + + RefreshState(); } std::filesystem::path @@ -205,21 +247,52 @@ HttpWorkspacesService::GetStatePath() const } void -HttpWorkspacesService::ReadState() +HttpWorkspacesService::RefreshState() { if (!m_Config.SystemRootDir.empty()) { - m_Workspaces.ReadState(GetStatePath(), [](const std::filesystem::path& Path) { return PathToChunkId(Path); }); + m_Workspaces.RefreshState(GetStatePath()); } } +bool +HttpWorkspacesService::MayChangeConfiguration(const HttpServerRequest& Req) const +{ + ZEN_UNUSED(Req); + return m_Config.AllowConfigurationChanges; +} + void -HttpWorkspacesService::WriteState() +HttpWorkspacesService::RefreshRequest(HttpRouterRequest& Req) { - if (!m_Config.SystemRootDir.empty()) + HttpServerRequest& ServerRequest = Req.ServerRequest(); + RefreshState(); + return ServerRequest.WriteResponse(HttpResponseCode::OK); +} + +void +HttpWorkspacesService::WorkspacesRequest(HttpRouterRequest& Req) +{ + HttpServerRequest& ServerRequest = Req.ServerRequest(); + + std::vector<Oid> WorkspaceIds = m_Workspaces.GetWorkspaces(); + CbObjectWriter Response; + Response.BeginArray("workspaces"); + for (const Oid& WorkspaceId : WorkspaceIds) { - m_Workspaces.WriteState(GetStatePath()); + if (std::optional<Workspaces::WorkspaceConfiguration> WorkspaceConfig = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); + WorkspaceConfig) + { + Response.BeginObject(); + { + WriteWorkspaceAndSharesConfig(Response, m_Workspaces, *WorkspaceConfig); + } + Response.EndObject(); + } } + Response.EndArray(); + + return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } void @@ -397,10 +470,11 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) HttpContentType::kText, "Invalid 'root_path' parameter"); } + if (Req.GetCapture(1) == Oid::Zero.ToString()) { // Synthesize Id - WorkspaceId = PathToChunkId(WorkspacePath); + WorkspaceId = Workspaces::PathToId(WorkspacePath); ZEN_INFO("Generated workspace id from path '{}': {}", WorkspacePath, WorkspaceId); } else if (WorkspaceId == Oid::Zero) @@ -410,25 +484,54 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } - m_WorkspacesStats.WorkspaceWriteCount++; - Workspaces::WorkspaceConfiguration NewConfig = {.Id = WorkspaceId, .RootPath = WorkspacePath}; - bool OK = m_Workspaces.AddWorkspace(NewConfig); - if (OK) + + if (!MayChangeConfiguration(ServerRequest)) { - WriteState(); - return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", WorkspaceId)); + return ServerRequest.WriteResponse(HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Adding workspace {} is not allowed", WorkspaceId)); } - else + bool AllowShareCreationFromHttp = false; + if (std::string_view Value = ServerRequest.GetQueryParams().GetValue("allow_share_creation_from_http"); Value == "true"sv) + { + AllowShareCreationFromHttp = true; + } + + m_WorkspacesStats.WorkspaceWriteCount++; + Workspaces::WorkspaceConfiguration OldConfig = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); + Workspaces::WorkspaceConfiguration NewConfig = {.Id = WorkspaceId, + .RootPath = WorkspacePath, + .AllowShareCreationFromHttp = AllowShareCreationFromHttp}; + if (OldConfig.Id == WorkspaceId && (OldConfig != NewConfig)) { - Workspaces::WorkspaceConfiguration Config = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); - if (Config == NewConfig) - { - return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", WorkspaceId)); - } return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, - fmt::format("Workspace {} already exists with root path '{}'", WorkspaceId, Config.RootPath)); + fmt::format("Workspace {} already exists with root path '{}'", WorkspaceId, OldConfig.RootPath)); + } + else if (OldConfig.Id == Oid::Zero) + { + if (Workspaces::WorkspaceConfiguration ConfigWithSameRoot = + Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspacePath); + ConfigWithSameRoot.Id != Oid::Zero) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Conflict, + HttpContentType::kText, + fmt::format("Workspace {} already exists with same root path '{}'", ConfigWithSameRoot.Id, WorkspacePath)); + } + } + + bool Created = Workspaces::AddWorkspace(Log(), GetStatePath(), NewConfig); + if (Created) + { + ZEN_ASSERT(OldConfig.Id == Oid::Zero); + RefreshState(); + return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", WorkspaceId)); + } + else + { + return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", WorkspaceId)); } } case HttpVerb::kGet: @@ -441,31 +544,17 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } m_WorkspacesStats.WorkspaceReadCount++; - Workspaces::WorkspaceInfo Info = m_Workspaces.GetWorkspaceInfo(WorkspaceId); - if (Info.Config.Id != Oid::Zero) + std::optional<Workspaces::WorkspaceConfiguration> Workspace = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); + if (Workspace) { CbObjectWriter Response; - Response << "id" << Info.Config.Id; - Response << "root_path" << Info.Config.RootPath.string(); // utf8? - Response.BeginArray("shares"); - for (const Workspaces::WorkspaceShareConfiguration& ShareConfig : Info.Shares) - { - Response.BeginObject(); - { - Response << "id" << ShareConfig.Id; - Response << "share_path" << ShareConfig.SharePath.string(); // utf8? - if (!ShareConfig.Alias.empty()) - { - Response << "alias" << ShareConfig.Alias; - } - } - Response.EndObject(); - } - Response.EndArray(); - + WriteWorkspaceAndSharesConfig(Response, m_Workspaces, *Workspace); return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } - return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + else + { + return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + } } case HttpVerb::kDelete: { @@ -476,11 +565,19 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } + + if (!MayChangeConfiguration(ServerRequest)) + { + return ServerRequest.WriteResponse(HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Removing workspace {} is not allowed", WorkspaceId)); + } + m_WorkspacesStats.WorkspaceDeleteCount++; - bool Deleted = m_Workspaces.RemoveWorkspace(WorkspaceId); + bool Deleted = Workspaces::RemoveWorkspace(Log(), GetStatePath(), WorkspaceId); if (Deleted) { - WriteState(); + RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::OK); } return ServerRequest.WriteResponse(HttpResponseCode::NotFound); @@ -938,50 +1035,88 @@ HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req, const Oid& Workspace if (ShareId == Oid::Zero) { // Synthesize Id - ShareId = PathToChunkId(SharePath); + ShareId = Workspaces::PathToId(SharePath); ZEN_INFO("Generated workspace id from path '{}': {}", SharePath, ShareId); } std::string Alias = HttpServerRequest::Decode(ServerRequest.GetQueryParams().GetValue("alias"sv)); - if (!AsciiSet::HasOnly(Alias, ValidAliasCharactersSet)) + if (!AsciiSet::HasOnly(Alias, Workspaces::ValidAliasCharactersSet)) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid 'alias' parameter"); } - m_WorkspacesStats.WorkspaceShareWriteCount++; - if (m_Workspaces.GetWorkspaceInfo(WorkspaceId).Config.Id != WorkspaceId) + Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); + if (Workspace.Id == Oid::Zero) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("Workspace '{}' does not exist", WorkspaceId)); } + + if (!Workspace.AllowShareCreationFromHttp) + { + if (!MayChangeConfiguration(ServerRequest)) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Adding workspace share {} in workspace {} is not allowed", WorkspaceId, ShareId)); + } + } + + m_WorkspacesStats.WorkspaceShareWriteCount++; + + const Workspaces::WorkspaceShareConfiguration OldConfig = + Workspaces::FindWorkspaceShare(Log(), Workspace.RootPath, ShareId); const Workspaces::WorkspaceShareConfiguration NewConfig = {.Id = ShareId, .SharePath = SharePath, .Alias = std::string(Alias)}; - bool OK = m_Workspaces.AddWorkspaceShare(WorkspaceId, NewConfig, [](const std::filesystem::path& Path) { - return PathToChunkId(Path); - }); - if (OK) - { - WriteState(); - return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", ShareId)); - } - else + + if (OldConfig.Id == ShareId && (OldConfig != NewConfig)) { - Workspaces::WorkspaceShareConfiguration Config = m_Workspaces.GetWorkspaceShareConfiguration(WorkspaceId, ShareId); - if (Config == NewConfig) - { - return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", ShareId)); - } return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, fmt::format("Workspace share '{}' already exist in workspace '{}' with share path '{}' and alias '{}'", ShareId, WorkspaceId, - Config.SharePath, - Config.Alias)); + OldConfig.SharePath, + OldConfig.Alias)); + } + else if (OldConfig.Id == Oid::Zero) + { + if (Workspaces::WorkspaceShareConfiguration ConfigWithSamePath = + Workspaces::FindWorkspaceShare(Log(), Workspace.RootPath, SharePath); + ConfigWithSamePath.Id != Oid::Zero) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Conflict, + HttpContentType::kText, + fmt::format("Workspace share '{}' already exist in workspace '{}' with same share path '{}' and alias '{}'", + ShareId, + WorkspaceId, + OldConfig.SharePath, + OldConfig.Alias)); + } + } + + if (!std::filesystem::is_directory(Workspace.RootPath / NewConfig.SharePath)) + { + return ServerRequest.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("directory {} does not exist in workspace {} root '{}'", + NewConfig.SharePath, + WorkspaceId, + Workspace.RootPath)); + } + + bool Created = Workspaces::AddWorkspaceShare(Log(), Workspace.RootPath, NewConfig); + if (Created) + { + RefreshState(); + return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", ShareId)); } + return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", ShareId)); } case HttpVerb::kGet: { @@ -999,20 +1134,18 @@ HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req, const Oid& Workspace HttpContentType::kText, fmt::format("Invalid share id '{}'", ShareId)); } + m_WorkspacesStats.WorkspaceShareReadCount++; - Workspaces::WorkspaceShareConfiguration Config = m_Workspaces.GetWorkspaceShareConfiguration(WorkspaceId, ShareId); - if (Config.Id != Oid::Zero) + std::optional<Workspaces::WorkspaceShareConfiguration> Config = + m_Workspaces.GetWorkspaceShareConfiguration(WorkspaceId, ShareId); + if (!Config) { - CbObjectWriter Response; - Response << "id" << Config.Id; - Response << "share_path" << Config.SharePath.string(); // utf8? - if (!Config.Alias.empty()) - { - Response << "alias" << Config.Alias; - } - return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); + return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } - return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + + CbObjectWriter Response; + WriteWorkspaceShareConfig(Response, *Config); + return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } case HttpVerb::kDelete: { @@ -1030,11 +1163,29 @@ HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req, const Oid& Workspace HttpContentType::kText, fmt::format("Invalid share id '{}'", ShareId)); } + + Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); + if (Workspace.Id == Oid::Zero) + { + return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + } + + if (!Workspace.AllowShareCreationFromHttp) + { + if (!MayChangeConfiguration(ServerRequest)) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Removing workspace share {} in workspace {} is not allowed", WorkspaceId, ShareId)); + } + } + m_WorkspacesStats.WorkspaceShareDeleteCount++; - bool Deleted = m_Workspaces.RemoveWorkspaceShare(WorkspaceId, ShareId); + bool Deleted = Workspaces::RemoveWorkspaceShare(Log(), Workspace.RootPath, ShareId); if (Deleted) { - WriteState(); + RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::OK); } return ServerRequest.WriteResponse(HttpResponseCode::NotFound); |