|
|
@@ -222,6 +222,15 @@ void CredentialController::registerRoutes(httplib::Server& server) {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+ server.Post(R"(/api/v1/credentials/([^/]+)/transfer-owner)",
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
+ middleware_.requireAuth(req, res, [this](const httplib::Request& req,
|
|
|
+ httplib::Response& res,
|
|
|
+ const auth::AuthContext& ctx) {
|
|
|
+ transferCredentialOwner(req, res, ctx);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
LOG_INFO("Credential API routes registered");
|
|
|
}
|
|
|
|
|
|
@@ -430,6 +439,79 @@ void CredentialController::refreshToken(const httplib::Request& req, httplib::Re
|
|
|
sendJson(res, {{"success", true}});
|
|
|
}
|
|
|
|
|
|
+bool CredentialController::checkTransferTarget(httplib::Response& res, const std::string& new_owner_id,
|
|
|
+ const std::string& project_id) {
|
|
|
+ if (new_owner_id.empty()) {
|
|
|
+ sendError(res, "newOwnerId is required", 400);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto user = storage_.get("users", new_owner_id);
|
|
|
+ if (user.failed()) {
|
|
|
+ sendError(res, "Unknown user id", 400);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // The new owner has to be somebody who can actually reach the project this
|
|
|
+ // credential lives in, the same rule transferWorkflowOwner uses - a
|
|
|
+ // transfer that names somebody who cannot see the project is worse than an
|
|
|
+ // error that says why.
|
|
|
+ auth::AuthContext target_ctx;
|
|
|
+ target_ctx.user_id = new_owner_id;
|
|
|
+ target_ctx.role = user.value().value("role", "user");
|
|
|
+ if (access_.roleIn(target_ctx, project_id) == auth::ProjectRole::None) {
|
|
|
+ sendError(res, "That user has no access to the project this belongs to. "
|
|
|
+ "Add them to the project before transferring ownership to them.", 400);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+void CredentialController::transferCredentialOwner(const httplib::Request& req, httplib::Response& res,
|
|
|
+ const auth::AuthContext& ctx) {
|
|
|
+ const std::string id = req.matches[1].str();
|
|
|
+
|
|
|
+ auto existing = credential_store_.get(id);
|
|
|
+ if (existing.failed()) {
|
|
|
+ sendError(res, "Credential not found", 404);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 404 rather than 403 for somebody who cannot see it at all: whether a
|
|
|
+ // credential exists is only told to people who can reach it.
|
|
|
+ if (!mayUse(ctx, existing.value())) {
|
|
|
+ sendError(res, "Credential not found", 404);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Manage, not Write: transferring somebody's credential is an
|
|
|
+ // administrative act on the project, not an edit to the credential
|
|
|
+ // itself - the same distinction transferWorkflowOwner draws.
|
|
|
+ if (!access_.allowed(ctx, existing.value().project_id, auth::Action::Manage)) {
|
|
|
+ sendError(res, "Only a project admin can transfer ownership of this credential", 403);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ nlohmann::json body;
|
|
|
+ try {
|
|
|
+ body = nlohmann::json::parse(req.body);
|
|
|
+ } catch (const std::exception&) {
|
|
|
+ sendError(res, "Invalid request body", 400);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const std::string new_owner_id = body.value("newOwnerId", "");
|
|
|
+ if (!checkTransferTarget(res, new_owner_id, existing.value().project_id)) return;
|
|
|
+
|
|
|
+ auto result = credential_store_.setOwner(id, new_owner_id);
|
|
|
+ if (result.failed()) {
|
|
|
+ sendError(res, result.error().message(), 500);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ LOG_INFO("Credential {} ownership transferred from {} to {} by {}", id,
|
|
|
+ existing.value().owner_id, new_owner_id, ctx.user_id);
|
|
|
+ sendJson(res, result.value().toJson());
|
|
|
+}
|
|
|
+
|
|
|
void CredentialController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
|
|
|
res.status = status;
|
|
|
res.set_content(data.dump(), "application/json");
|