aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/workspaces/httpworkspaces.cpp
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2024-10-23 10:31:43 +0200
committerGitHub Enterprise <[email protected]>2024-10-23 10:31:43 +0200
commit530ab3394938331f224058c381a1db5d4a68e6a9 (patch)
tree5060eb394d67b7454855aed0fa8d7d3acf5f5c98 /src/zenserver/workspaces/httpworkspaces.cpp
parentfix gc date (#204) (diff)
downloadzen-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.cpp321
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);